Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db9e09c06f |
-59
@@ -1,59 +0,0 @@
|
|||||||
# Dependencies
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Lock files (keep package-lock.json for consistency)
|
|
||||||
bun.lock
|
|
||||||
yarn.lock
|
|
||||||
|
|
||||||
# Environment variables
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage/
|
|
||||||
|
|
||||||
# Development and build tools
|
|
||||||
.kiro/
|
|
||||||
.github/
|
|
||||||
.npm-cache/
|
|
||||||
.cache/
|
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
|
|
||||||
# OS generated files
|
|
||||||
.DS_Store
|
|
||||||
.DS_Store?
|
|
||||||
._*
|
|
||||||
.Spotlight-V100
|
|
||||||
.Trashes
|
|
||||||
ehthumbs.db
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# IDE files
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
|
|
||||||
# Temporary files
|
|
||||||
*.tmp
|
|
||||||
*.temp
|
|
||||||
|
|
||||||
# Local configuration files
|
|
||||||
.gimsrc
|
|
||||||
-23
@@ -1,23 +0,0 @@
|
|||||||
# NPM Ignore
|
|
||||||
.git
|
|
||||||
.github
|
|
||||||
.gitignore
|
|
||||||
.DS_Store
|
|
||||||
.env*
|
|
||||||
test/
|
|
||||||
tests/
|
|
||||||
examples/
|
|
||||||
scripts/
|
|
||||||
docs/
|
|
||||||
mcp/
|
|
||||||
AI.md
|
|
||||||
*.log
|
|
||||||
coverage/
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Keep bin and library code
|
|
||||||
!bin/
|
|
||||||
!package.json
|
|
||||||
!README.md
|
|
||||||
!LICENSE
|
|
||||||
!CHANGELOG.md
|
|
||||||
-189
@@ -1,189 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
## [0.8.1] - 2025-12-19
|
|
||||||
|
|
||||||
### 🚀 Smart Sync Fix
|
|
||||||
New `g fix` command to handle branch sync issues (diverged/ahead/behind):
|
|
||||||
|
|
||||||
| Option | What it does |
|
|
||||||
|--------|--------------|
|
|
||||||
| `g fix` | Show status and available options |
|
|
||||||
| `g fix --ai` | Get AI recommendation for best approach |
|
|
||||||
| `g fix --merge` | Merge remote into local |
|
|
||||||
| `g fix --rebase` | Rebase local onto remote |
|
|
||||||
| `g fix --local --yes` | Force push local to remote |
|
|
||||||
| `g fix --remote --yes` | Reset to remote, discard local |
|
|
||||||
|
|
||||||
### 🔧 Conflict Helper
|
|
||||||
New `g conflicts` command shows conflicted files and how to resolve them.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [0.8.0] - 2025-12-19
|
|
||||||
|
|
||||||
### 🚀 New Workflow Shorthand Commands
|
|
||||||
Multi-step git workflows simplified to single commands:
|
|
||||||
|
|
||||||
| Command | Alias | What it does |
|
|
||||||
|---------|-------|--------------|
|
|
||||||
| `safe-pull` | `sp` | Stash → Pull → Stash pop (safe pull with uncommitted changes) |
|
|
||||||
| `main` | - | Switch to main/master and pull latest |
|
|
||||||
| `unstage` | `us` | Unstage all staged files |
|
|
||||||
| `discard` | `x` | Discard all changes (with --yes confirmation) |
|
|
||||||
| `stash-save` | `ss` | Quick stash all changes with auto-generated name |
|
|
||||||
| `stash-pop` | `pop` | Pop the latest stash |
|
|
||||||
| `delete-branch` | `del` | Delete branch locally and remotely |
|
|
||||||
| `cleanup` | `clean` | Remove local branches deleted from remote |
|
|
||||||
| `last` | - | Show last commit details and diff |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [0.7.2] - 2025-12-18
|
|
||||||
|
|
||||||
### 🐛 Bug Fix
|
|
||||||
- **Fixed progress spinner garbage output**: Spinner now properly clears the line when stopping, preventing partial text artifacts like ` (1ms)rating AI review ⠋`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [0.7.1] - 2025-12-18
|
|
||||||
|
|
||||||
### 🐛 Bug Fix
|
|
||||||
- **Fixed AI suggestions in interactive mode**: Multiple suggestions now correctly display message strings instead of `[object Object]`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [0.7.0] - 2025-12-18
|
|
||||||
|
|
||||||
### 🚀 New Intelligent Commands
|
|
||||||
- **`g wip`**: Quick work-in-progress commit - stage all and commit instantly
|
|
||||||
- **`g today` / `g t`**: Show all commits made today with timestamps
|
|
||||||
- **`g stats`**: Personal commit statistics with streak tracking, type breakdown, and style analysis
|
|
||||||
- **`g review` / `g r`**: AI code review before committing - shows complexity, detected patterns, and suggested message
|
|
||||||
- **`g split`**: Smart suggestions for splitting large changesets into atomic commits
|
|
||||||
|
|
||||||
### ✨ Enhanced Status
|
|
||||||
- **File type emojis**: 📄 JS, 🎨 CSS, 🧪 tests, ⚙️ config, 📦 package.json, etc.
|
|
||||||
- **Session awareness**: Shows time since last commit and daily commit count
|
|
||||||
- **Branch context**: Detects branch type from naming patterns (feat/, fix/, etc.)
|
|
||||||
- **Smarter insights**: Suggests staging, split commits, and more
|
|
||||||
|
|
||||||
### 🧠 Intelligence Module
|
|
||||||
- **Commit pattern analysis**: Learns your style from git history
|
|
||||||
- **Semantic change detection**: Identifies breaking changes, new features, bug fixes
|
|
||||||
- **Complexity analysis**: Visual indicators for simple/moderate/complex changes
|
|
||||||
|
|
||||||
### 📊 UX Improvements
|
|
||||||
- **Time elapsed display**: Shows how long operations take
|
|
||||||
- **Random tips**: Contextual tips to help learn GIMS features
|
|
||||||
- **Cached response indicators**: Know when AI cache is used
|
|
||||||
- **Better quick-help**: Reorganized command reference with new commands
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [0.6.7] - 2025-10-26
|
|
||||||
|
|
||||||
### 🔧 Fixes
|
|
||||||
- **Added `-v` flag for version**: Now `g -v` or `gims -v` shows the version number
|
|
||||||
- **Renamed command**: Changed `help-quick` to `quick-help` (alias `q` still works)
|
|
||||||
- **Added `push` command**: Now `g push` or `gims push` works as a standalone command
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [0.6.6] - 2025-10-26
|
|
||||||
|
|
||||||
### 🔧 Improvements
|
|
||||||
- **Removed commit message length restriction**: AI can now generate commit messages of any length (no more `...` truncation at 72 chars)
|
|
||||||
- **Added confirmation prompt for local heuristics**: When no AI provider is configured, the tool now asks for confirmation `[Y/n]` before committing
|
|
||||||
- Default is `Y` (just press Enter to accept)
|
|
||||||
- Warning message clearly indicates "local heuristics" instead of AI
|
|
||||||
- Can be bypassed with `--yes` flag for automation
|
|
||||||
- **Better user awareness**: Users are now informed when local heuristics are used instead of AI
|
|
||||||
|
|
||||||
### 📚 User Experience
|
|
||||||
- More transparent about when AI is used vs. local pattern matching
|
|
||||||
- Safer commits when API keys are not configured
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [0.6.5] - 2025-10-20
|
|
||||||
|
|
||||||
### 🔧 Command Behavior Updated
|
|
||||||
- **`g a` (amend) default behavior changed**: Now keeps the original commit message by default (`--no-edit`)
|
|
||||||
- **New `--edit` flag**: Use `g a --edit` to generate a new AI commit message
|
|
||||||
- **Simpler workflow**: `g a` simply merges current changes into previous commit without editing the message
|
|
||||||
|
|
||||||
### 📚 Documentation Updates
|
|
||||||
- Updated all documentation to reflect the new amend behavior
|
|
||||||
- Clarified that `g a` merges changes while keeping the original message
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [0.6.4] - 2025-10-19
|
|
||||||
|
|
||||||
### 🔧 Command Aliases Fixed
|
|
||||||
- **Fixed `g i` alias**: Now correctly initializes a git repository (was incorrectly mapped to interactive mode)
|
|
||||||
- **Updated `g interactive` alias**: Changed from `g i` to `g int` to avoid conflicts
|
|
||||||
- **Verified `g ls` and `g ll`**: Both commands working correctly (short and detailed commit history)
|
|
||||||
|
|
||||||
### 📚 Documentation Updates
|
|
||||||
- Updated `QUICK_REFERENCE.md` to reflect correct command aliases
|
|
||||||
- Updated help output (`g q`) to show correct mappings
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [0.6.2] - 2024-12-19
|
|
||||||
|
|
||||||
### 🚀 AI Model Updates
|
|
||||||
- **Updated to latest AI models**:
|
|
||||||
- Gemini: `gemini-2.5-flash` (latest and fastest)
|
|
||||||
- OpenAI: `gpt-5` (latest GPT model)
|
|
||||||
- Groq: `groq/compound` (latest Groq model)
|
|
||||||
|
|
||||||
### 📚 Documentation
|
|
||||||
- **Simplified README**: Reduced from verbose to concise format
|
|
||||||
- **Focused on essentials**: Quick start, main commands, examples
|
|
||||||
- **Better readability**: Easier to scan and understand
|
|
||||||
|
|
||||||
### 🧹 Repository Cleanup
|
|
||||||
- Removed development-specific files (.kiro/, .github/, coverage/)
|
|
||||||
- Updated .gitignore to exclude dev directories
|
|
||||||
- Cleaner package for npm distribution
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [0.6.1] - 2024-12-19
|
|
||||||
|
|
||||||
### ✨ Enhanced User Experience
|
|
||||||
- **Single-letter aliases** for main workflow commands
|
|
||||||
- **Quick API setup**: `g setup --api-key <provider>`
|
|
||||||
- **Better onboarding**: Clear setup instructions with direct links
|
|
||||||
- **Quick help**: `g q` command for instant reference
|
|
||||||
|
|
||||||
### 🔧 Improved Commands
|
|
||||||
- `g s` - Status (enhanced git status)
|
|
||||||
- `g i` - Interactive (guided commit wizard)
|
|
||||||
- `g p` - Preview (see what will be committed)
|
|
||||||
- `g l` - Local (AI commit locally)
|
|
||||||
- `g o` - Online (AI commit + push)
|
|
||||||
- `g h` - History (numbered commit log)
|
|
||||||
- `g a` - Amend (smart amend with AI)
|
|
||||||
- `g u` - Undo (undo last commit)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [0.6.0] - 2024-12-19
|
|
||||||
|
|
||||||
### 🚀 Major Enhanced Release
|
|
||||||
- **Interactive commit wizard** with multiple AI suggestions
|
|
||||||
- **Enhanced git status** with AI insights and project analysis
|
|
||||||
- **Modular architecture** for better performance and maintainability
|
|
||||||
- **Smart configuration** with project type detection
|
|
||||||
- **Progress indicators** and better error handling
|
|
||||||
- **Caching system** for improved AI response times
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [0.5.4] - Previous Version
|
|
||||||
- Basic AI-powered commit messages
|
|
||||||
- Simple configuration
|
|
||||||
- Core git operations
|
|
||||||
@@ -1,21 +1,18 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 Sairaj Jawalikar
|
Copyright (c) 2026 s41r4j
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
in the Software without restriction, including without limitation the rights
|
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
following conditions:
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
copies or substantial portions of the Software.
|
portions of the Software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|||||||
@@ -1,385 +1,2 @@
|
|||||||
<div align="center">
|
# gims
|
||||||
|
|
||||||
<!--
|
|
||||||
# GIMS - Git Made Simple
|
|
||||||
-->
|
|
||||||
|
|
||||||
<img width="1658" height="609" alt="gims" src="https://github.com/user-attachments/assets/fd4ae515-9674-4cc2-8ea0-39d21e82e274" />
|
|
||||||
|
|
||||||
[](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.
|
Before Width: | Height: | Size: 6.9 MiB |
-1867
File diff suppressed because it is too large
Load Diff
@@ -1,132 +0,0 @@
|
|||||||
/**
|
|
||||||
* GIMS AI Interface Registry
|
|
||||||
*
|
|
||||||
* This module defines the "Tools" that GIMS exposes to AI systems (MCP, OpenAI, etc.).
|
|
||||||
* It serves as the Single Source of Truth for tool definitions and their schemas.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const simpleGit = require('simple-git');
|
|
||||||
const { GitAnalyzer } = require('../git/analyzer');
|
|
||||||
const { S4Versioning } = require('../utils/s4');
|
|
||||||
const { ConfigManager } = require('../config/manager');
|
|
||||||
const { AIProviderManager } = require('./providers');
|
|
||||||
|
|
||||||
// Initialize shared instances
|
|
||||||
const git = simpleGit();
|
|
||||||
const gitAnalyzer = new GitAnalyzer(git);
|
|
||||||
const configManager = new ConfigManager();
|
|
||||||
const config = configManager.load();
|
|
||||||
const aiProvider = new AIProviderManager(config);
|
|
||||||
const s4 = new S4Versioning(git);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tool Definitions
|
|
||||||
* Format:
|
|
||||||
* {
|
|
||||||
* name: "tool_name",
|
|
||||||
* description: "Description for the AI",
|
|
||||||
* inputSchema: { ...JSON Schema... },
|
|
||||||
* handler: async (args) => { ... implementation ... }
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
const tools = [
|
|
||||||
{
|
|
||||||
name: "get_status",
|
|
||||||
description: "Get enhanced git status with AI insights, file modification stats, and branch context. Use this to understand the current state of the repository.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {},
|
|
||||||
},
|
|
||||||
handler: async () => {
|
|
||||||
return await gitAnalyzer.getEnhancedStatus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "analyze_history",
|
|
||||||
description: "Analyze recent commit history, including authors, conventions, and activity patterns.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
limit: {
|
|
||||||
type: "number",
|
|
||||||
description: "Number of commits to analyze (default: 10)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handler: async ({ limit = 10 }) => {
|
|
||||||
return await gitAnalyzer.analyzeCommitHistory(limit);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "version_info",
|
|
||||||
description: "Get current project version information using S4 versioning (Semantic + Date/Time).",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {}
|
|
||||||
},
|
|
||||||
handler: async () => {
|
|
||||||
const currentVersion = await s4.getCurrentVersion();
|
|
||||||
if (!currentVersion) return { version: "0.0.0", source: "fallback" };
|
|
||||||
return { version: currentVersion, details: S4Versioning.parse(currentVersion) };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "generate_commit_message",
|
|
||||||
description: "Generate a high-quality commit message based on currently staged changes.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
context: {
|
|
||||||
type: "string",
|
|
||||||
description: "Optional context or user instruction for the message generation"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handler: async ({ context }) => {
|
|
||||||
const rawDiff = await git.diff(['--cached', '--no-ext-diff']);
|
|
||||||
if (!rawDiff.trim()) {
|
|
||||||
return { error: "No staged changes to generate message for." };
|
|
||||||
}
|
|
||||||
// Pass pseudo-options resembling CLI opts
|
|
||||||
const opts = { conventional: config.conventional };
|
|
||||||
const message = await aiProvider.generateCommitMessage(rawDiff, opts);
|
|
||||||
|
|
||||||
// Handle object return from provider (some return { message, usedLocal })
|
|
||||||
const msgStr = typeof message === 'string' ? message : message.message;
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: msgStr,
|
|
||||||
original_diff_size: rawDiff.length
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "run_git_command",
|
|
||||||
description: "Execute a raw git command safely. Use this for standard git operations not covered by other tools.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
command: {
|
|
||||||
type: "string",
|
|
||||||
description: "The full git command to run (e.g., 'log -n 5', 'branch --list')"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ["command"]
|
|
||||||
},
|
|
||||||
handler: async ({ command }) => {
|
|
||||||
// Safety check: block potentially destructive commands if needed,
|
|
||||||
// but for now we assume the agent is trusted or user-monitored.
|
|
||||||
// We'll strip 'git ' prefix if present.
|
|
||||||
const cleanCmd = command.replace(/^git\s+/, '').trim();
|
|
||||||
const args = cleanCmd.split(' ');
|
|
||||||
try {
|
|
||||||
const result = await git.raw(args);
|
|
||||||
return { output: result ? result.trim() : "Success" };
|
|
||||||
} catch (e) {
|
|
||||||
return { error: e.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
module.exports = { tools };
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
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.cache = new Map();
|
|
||||||
this.maxCacheSize = 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolveProvider(preference = 'auto') {
|
|
||||||
if (preference === 'none') return 'none';
|
|
||||||
|
|
||||||
// Check if preferred provider's key is available
|
|
||||||
if (preference === 'openai' && process.env.OPENAI_API_KEY) return 'openai';
|
|
||||||
if (preference === 'gemini' && process.env.GEMINI_API_KEY) return 'gemini';
|
|
||||||
if (preference === 'groq' && process.env.GROQ_API_KEY) return 'groq';
|
|
||||||
|
|
||||||
// Fallback: try any available provider (priority: Gemini → OpenAI → Groq)
|
|
||||||
if (process.env.GEMINI_API_KEY) return 'gemini';
|
|
||||||
if (process.env.OPENAI_API_KEY) return 'openai';
|
|
||||||
if (process.env.GROQ_API_KEY) return 'groq';
|
|
||||||
|
|
||||||
return 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultModel(provider) {
|
|
||||||
const defaults = {
|
|
||||||
'gemini': 'gemini-3-flash-preview', // Latest Gemini model
|
|
||||||
'openai': 'gpt-5.2-2025-12-11', // Latest GPT model
|
|
||||||
'groq': 'groq/compound' // Latest Groq model
|
|
||||||
};
|
|
||||||
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;
|
|
||||||
return this.cache.get(cacheKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
setCache(cacheKey, result, usedLocal = false) {
|
|
||||||
if (!this.config.cacheEnabled) return;
|
|
||||||
|
|
||||||
if (this.cache.size >= this.maxCacheSize) {
|
|
||||||
const firstKey = this.cache.keys().next().value;
|
|
||||||
this.cache.delete(firstKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cache.set(cacheKey, {
|
|
||||||
result,
|
|
||||||
usedLocal,
|
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async generateWithProvider(provider, prompt, options = {}) {
|
|
||||||
const { model = '', temperature = 0.3, maxTokens = 200 } = 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) {
|
|
||||||
throw new Error(`${provider} generation failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Check cache first
|
|
||||||
const cacheKey = this.getCacheKey(diff, { 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(diff, options);
|
|
||||||
this.setCache(cacheKey, result, true);
|
|
||||||
return { message: result, usedLocal: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
const prompt = this.buildPrompt(diff, { conventional, body });
|
|
||||||
const result = await this.generateWithProvider(provider, prompt, options);
|
|
||||||
const cleaned = this.cleanCommitMessage(result, { body });
|
|
||||||
|
|
||||||
this.setCache(cacheKey, cleaned, false);
|
|
||||||
return { message: cleaned, usedLocal: false };
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
if (verbose) Progress.warning(`${provider} failed: ${error.message}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 };
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
const readline = require('readline');
|
|
||||||
const { color } = require('../utils/colors');
|
|
||||||
const { Progress } = require('../utils/progress');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interactive commit wizard and user input utilities
|
|
||||||
*/
|
|
||||||
class InteractiveCommands {
|
|
||||||
constructor(git, aiProvider, analyzer) {
|
|
||||||
this.git = git;
|
|
||||||
this.aiProvider = aiProvider;
|
|
||||||
this.analyzer = analyzer;
|
|
||||||
}
|
|
||||||
|
|
||||||
async promptUser(question, defaultValue = '') {
|
|
||||||
const rl = readline.createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
output: process.stdout
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise(resolve => {
|
|
||||||
rl.question(question, answer => {
|
|
||||||
rl.close();
|
|
||||||
resolve(answer.trim() || defaultValue);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async selectFromList(items, prompt = 'Select an option:', allowCustom = false) {
|
|
||||||
console.log(`\n${color.bold(prompt)}`);
|
|
||||||
|
|
||||||
items.forEach((item, index) => {
|
|
||||||
console.log(` ${color.cyan((index + 1).toString())}. ${item}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (allowCustom) {
|
|
||||||
console.log(` ${color.cyan('c')}. Custom message`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxChoice = items.length;
|
|
||||||
const validChoices = Array.from({length: maxChoice}, (_, i) => (i + 1).toString());
|
|
||||||
if (allowCustom) validChoices.push('c');
|
|
||||||
|
|
||||||
let choice;
|
|
||||||
do {
|
|
||||||
choice = await this.promptUser(`\nChoice (1-${maxChoice}${allowCustom ? ', c' : ''}): `);
|
|
||||||
} while (!validChoices.includes(choice));
|
|
||||||
|
|
||||||
if (choice === 'c') {
|
|
||||||
return await this.promptUser('Enter custom message: ');
|
|
||||||
}
|
|
||||||
|
|
||||||
return items[parseInt(choice) - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
async runInteractiveCommit(options = {}) {
|
|
||||||
try {
|
|
||||||
console.log(color.bold('\n🎯 Interactive Commit Wizard\n'));
|
|
||||||
|
|
||||||
// Step 1: Check for changes
|
|
||||||
Progress.step(1, 4, 'Analyzing changes...');
|
|
||||||
const status = await this.git.status();
|
|
||||||
|
|
||||||
if (status.files.length === 0) {
|
|
||||||
Progress.warning('No changes detected');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Show status and get staging preference
|
|
||||||
Progress.step(2, 4, 'Reviewing file changes...');
|
|
||||||
const enhancedStatus = await this.analyzer.getEnhancedStatus();
|
|
||||||
console.log(this.analyzer.formatStatusOutput(enhancedStatus));
|
|
||||||
|
|
||||||
// Ask about staging
|
|
||||||
let shouldStage = false;
|
|
||||||
if (status.staged.length === 0) {
|
|
||||||
const stageChoice = await this.promptUser(
|
|
||||||
'No files are staged. Stage all changes? (y/n) [y]: ',
|
|
||||||
'y'
|
|
||||||
);
|
|
||||||
shouldStage = stageChoice.toLowerCase() === 'y';
|
|
||||||
|
|
||||||
if (shouldStage) {
|
|
||||||
await this.git.add('.');
|
|
||||||
Progress.success('All changes staged');
|
|
||||||
} else {
|
|
||||||
console.log('You can manually stage files with: git add <file>');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Generate commit message suggestions
|
|
||||||
Progress.step(3, 4, 'Generating AI suggestions...');
|
|
||||||
Progress.start('🤖 AI is analyzing your changes');
|
|
||||||
|
|
||||||
const diff = await this.git.diff(['--cached', '--no-ext-diff']);
|
|
||||||
if (!diff.trim()) {
|
|
||||||
Progress.stop(color.yellow('No staged changes to commit'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const suggestions = await this.aiProvider.generateMultipleSuggestions(diff, options, 3);
|
|
||||||
Progress.stop(color.green('✓ Generated suggestions'));
|
|
||||||
|
|
||||||
// Step 4: Let user choose
|
|
||||||
Progress.step(4, 4, 'Select commit message...');
|
|
||||||
const selectedMessage = await this.selectFromList(
|
|
||||||
suggestions,
|
|
||||||
'Choose a commit message:',
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
// Confirm and commit
|
|
||||||
console.log(`\nSelected message: ${color.green(selectedMessage)}`);
|
|
||||||
const confirm = await this.promptUser('Proceed with commit? (y/n) [y]: ', 'y');
|
|
||||||
|
|
||||||
if (confirm.toLowerCase() === 'y') {
|
|
||||||
if (options.dryRun) {
|
|
||||||
console.log(color.yellow('[dry-run] Would commit with message:'));
|
|
||||||
console.log(selectedMessage);
|
|
||||||
} else {
|
|
||||||
await this.git.commit(selectedMessage);
|
|
||||||
Progress.success(`Committed: "${selectedMessage}"`);
|
|
||||||
|
|
||||||
// Ask about pushing
|
|
||||||
const pushChoice = await this.promptUser('Push to remote? (y/n) [n]: ', 'n');
|
|
||||||
if (pushChoice.toLowerCase() === 'y') {
|
|
||||||
try {
|
|
||||||
await this.git.push();
|
|
||||||
Progress.success('Pushed to remote');
|
|
||||||
} catch (error) {
|
|
||||||
Progress.error(`Push failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('Commit cancelled');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
Progress.error(`Interactive commit failed: ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async runQuickCommit(message, options = {}) {
|
|
||||||
try {
|
|
||||||
const status = await this.git.status();
|
|
||||||
|
|
||||||
if (status.files.length === 0) {
|
|
||||||
console.log('No changes to commit');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-stage if nothing is staged
|
|
||||||
if (status.staged.length === 0) {
|
|
||||||
console.log(color.yellow('Auto-staging all changes...'));
|
|
||||||
await this.git.add('.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
|
||||||
console.log(color.yellow('[dry-run] Would commit with message:'));
|
|
||||||
console.log(message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.git.commit(message);
|
|
||||||
Progress.success(`Committed: "${message}"`);
|
|
||||||
|
|
||||||
if (options.push) {
|
|
||||||
try {
|
|
||||||
await this.git.push();
|
|
||||||
Progress.success('Pushed to remote');
|
|
||||||
} catch (error) {
|
|
||||||
Progress.warning(`Push failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
Progress.error(`Quick commit failed: ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async showCommitPreview(options = {}) {
|
|
||||||
try {
|
|
||||||
const status = await this.git.status();
|
|
||||||
|
|
||||||
if (status.files.length === 0) {
|
|
||||||
console.log('No changes to preview');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show what would be committed
|
|
||||||
const diff = await this.git.diff(['--cached', '--no-ext-diff']);
|
|
||||||
if (!diff.trim()) {
|
|
||||||
console.log(color.yellow('No staged changes. Use --all to stage everything.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(color.bold('\n📋 Commit Preview\n'));
|
|
||||||
|
|
||||||
// Show file summary
|
|
||||||
const complexity = await this.analyzer.getChangeComplexity(diff);
|
|
||||||
console.log(`Complexity: ${color.cyan(complexity.complexity)}`);
|
|
||||||
console.log(`Files: ${complexity.files}, +${complexity.additions} -${complexity.deletions}`);
|
|
||||||
|
|
||||||
// Generate and show AI suggestion
|
|
||||||
Progress.start('🤖 Generating commit message');
|
|
||||||
const suggestion = await this.aiProvider.generateCommitMessage(diff, options);
|
|
||||||
Progress.stop('');
|
|
||||||
|
|
||||||
console.log(`\nSuggested message: ${color.green(suggestion)}`);
|
|
||||||
|
|
||||||
// Show diff summary (first few lines)
|
|
||||||
console.log(`\n${color.bold('Changes:')}`);
|
|
||||||
const diffLines = diff.split('\n').slice(0, 20);
|
|
||||||
diffLines.forEach(line => {
|
|
||||||
if (line.startsWith('+')) {
|
|
||||||
console.log(color.green(line));
|
|
||||||
} else if (line.startsWith('-')) {
|
|
||||||
console.log(color.red(line));
|
|
||||||
} else if (line.startsWith('@@')) {
|
|
||||||
console.log(color.cyan(line));
|
|
||||||
} else {
|
|
||||||
console.log(color.dim(line));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (diff.split('\n').length > 20) {
|
|
||||||
console.log(color.dim('... (truncated)'));
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
Progress.error(`Preview failed: ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { InteractiveCommands };
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const { S4Versioning } = require('../utils/s4');
|
|
||||||
const { color } = require('../utils/colors');
|
|
||||||
const { Progress } = require('../utils/progress');
|
|
||||||
|
|
||||||
class VersionCommand {
|
|
||||||
constructor(git, configManager) {
|
|
||||||
this.git = git;
|
|
||||||
this.configManager = configManager;
|
|
||||||
this.s4 = new S4Versioning(git);
|
|
||||||
}
|
|
||||||
|
|
||||||
async run(type = 'auto', options = {}) {
|
|
||||||
const { info, dryRun, stage, list, prune, all } = options;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// --- Special Commands: List & Prune ---
|
|
||||||
if (list) {
|
|
||||||
await this.listTags(all);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prune) {
|
|
||||||
await this.pruneTags();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.undo) {
|
|
||||||
await this.undoLastBump();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Main Bump Logic ---
|
|
||||||
|
|
||||||
// 1. Get current version from Git Tags (SSOT)
|
|
||||||
let currentVersion = await this.s4.getCurrentVersion();
|
|
||||||
let source = 'git-tag';
|
|
||||||
|
|
||||||
// Fallback: If no tags, check package.json just for migration base
|
|
||||||
if (!currentVersion) {
|
|
||||||
currentVersion = this.getPackageJsonVersion() || '0.0.0';
|
|
||||||
source = 'package.json (fallback)';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Info mode
|
|
||||||
if (info) {
|
|
||||||
console.log(`Current Version: ${color.green(currentVersion)} (${source})`);
|
|
||||||
this.showInfo(currentVersion);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Calculate next version
|
|
||||||
Progress.start('Calculated next version...');
|
|
||||||
const nextVersion = await this.s4.bump(currentVersion, type, stage);
|
|
||||||
Progress.stop('');
|
|
||||||
|
|
||||||
// 4. Dry run
|
|
||||||
if (dryRun) {
|
|
||||||
console.log(color.bold('\nDry Run Results:'));
|
|
||||||
console.log(`Current: ${color.dim(currentVersion)}`);
|
|
||||||
console.log(`Next: ${color.green(nextVersion)}`);
|
|
||||||
this.showInfo(nextVersion);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Execution (Tag + .env)
|
|
||||||
console.log(`\nBumping version: ${color.dim(currentVersion)} → ${color.green(nextVersion)}`);
|
|
||||||
|
|
||||||
const updatedFiles = [];
|
|
||||||
|
|
||||||
// Update .env (Only if exists)
|
|
||||||
const envUpdates = this.updateEnvFiles(nextVersion);
|
|
||||||
updatedFiles.push(...envUpdates);
|
|
||||||
|
|
||||||
if (updatedFiles.length > 0) {
|
|
||||||
console.log(`${color.green('✔')} Updated ${updatedFiles.join(', ')}`);
|
|
||||||
// Stage .env updates if any (usually gitignored, but if not...)
|
|
||||||
// We won't auto-commit them to avoid messing with user's git config too much logic
|
|
||||||
// But usually .env is ignored.
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Create Git Tag (The Truth)
|
|
||||||
await this.createTag(nextVersion);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
Progress.error(`Version bump failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getPackageJsonVersion() {
|
|
||||||
if (fs.existsSync('package.json')) {
|
|
||||||
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
||||||
return pkg.version;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateEnvFiles(newVersion) {
|
|
||||||
const updates = [];
|
|
||||||
const envFiles = ['.env', '.env.local', '.env.development', '.env.production'];
|
|
||||||
|
|
||||||
envFiles.forEach(file => {
|
|
||||||
if (fs.existsSync(file)) {
|
|
||||||
let content = fs.readFileSync(file, 'utf8');
|
|
||||||
let changed = false;
|
|
||||||
|
|
||||||
// Match common version vars
|
|
||||||
const regex = /^(VERSION|APP_VERSION|NEXT_PUBLIC_VERSION|REACT_APP_VERSION|S4_VERSION)=(.*)$/gm;
|
|
||||||
|
|
||||||
if (regex.test(content)) {
|
|
||||||
content = content.replace(regex, `$1=${newVersion}`);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
fs.writeFileSync(file, content);
|
|
||||||
updates.push(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return updates;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createTag(version) {
|
|
||||||
try {
|
|
||||||
// Create lightweight or annotated tag? S4 recommends annotated.
|
|
||||||
await this.git.addAnnotatedTag(version, `Release ${version}`);
|
|
||||||
console.log(`${color.green('✔')} Created git tag: ${color.cyan(version)}`);
|
|
||||||
console.log(`\nRun ${color.cyan('g push --tags')} to publish`);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(color.yellow(`⚠ Git tag failed: ${e.message}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async listTags(showAll) {
|
|
||||||
const tags = await this.git.tags();
|
|
||||||
if (!tags.all.length) {
|
|
||||||
console.log('No tags found.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(color.bold('\nRelease History:'));
|
|
||||||
|
|
||||||
// Reverse order (newest first)
|
|
||||||
// Filter out dev tags unless --all
|
|
||||||
const filtered = tags.all.filter(t => {
|
|
||||||
if (showAll) return true;
|
|
||||||
return !t.includes('-dev.');
|
|
||||||
}).reverse();
|
|
||||||
|
|
||||||
if (filtered.length === 0) {
|
|
||||||
console.log(color.dim('No stable/beta/rc tags found. Use --all to see dev tags.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
filtered.forEach(t => {
|
|
||||||
console.log(`- ${t}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async pruneTags() {
|
|
||||||
Progress.start('Pruning old dev tags...');
|
|
||||||
const tags = await this.git.tags();
|
|
||||||
const allTags = tags.all;
|
|
||||||
|
|
||||||
// Identify dev tags
|
|
||||||
const devTags = allTags.filter(t => t.includes('-dev.'));
|
|
||||||
|
|
||||||
// Sort them ensuring we keep the newest
|
|
||||||
// (Using simple string sort works roughly if S4 format is strictly kept,
|
|
||||||
// but better to rely on S4 parse if possible. For simplicity here, we assume standard S4)
|
|
||||||
|
|
||||||
// We want to KEEP the last 10 dev tags.
|
|
||||||
if (devTags.length <= 10) {
|
|
||||||
Progress.stop('');
|
|
||||||
console.log('Fewer than 10 dev tags. No pruning needed.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Identify tags to delete (older ones)
|
|
||||||
// S4 strings sort chronologically well enough lexicographically mostly,
|
|
||||||
// but let's trust the array order returned by git tags usually...
|
|
||||||
// actually git tags return is arbitrary.
|
|
||||||
|
|
||||||
// Let's use our S4 sorter from utility if possible, or just simple sort
|
|
||||||
// Assuming S4Versioning.getCurrentVersion logic uses sort, we can reuse it?
|
|
||||||
// Or just simple sort for now.
|
|
||||||
devTags.sort(); // Lexicographical sort (older dates come first: 2025... < 2026...)
|
|
||||||
|
|
||||||
const toKeep = 10;
|
|
||||||
const toDelete = devTags.slice(0, devTags.length - toKeep);
|
|
||||||
|
|
||||||
for (const tag of toDelete) {
|
|
||||||
await this.git.tag(['-d', tag]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Progress.stop('');
|
|
||||||
console.log(`${color.green('✔')} Deleted ${toDelete.length} old dev tags.`);
|
|
||||||
console.log(color.dim('Kept last 10 dev tags + all stable tags.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
async undoLastBump() {
|
|
||||||
Progress.start('Undoing last version bump...');
|
|
||||||
|
|
||||||
// 1. Get current version (the one to delete)
|
|
||||||
const currentVersion = await this.s4.getCurrentVersion();
|
|
||||||
|
|
||||||
if (!currentVersion) {
|
|
||||||
Progress.stop('');
|
|
||||||
console.log(color.yellow('No S4 version tags found to undo.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Delete the tag
|
|
||||||
try {
|
|
||||||
await this.git.tag(['-d', currentVersion]);
|
|
||||||
Progress.stop('');
|
|
||||||
console.log(`${color.green('✔')} Deleted tag: ${color.cyan(currentVersion)}`);
|
|
||||||
} catch (e) {
|
|
||||||
Progress.error(`Failed to delete tag: ${e.message}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Revert .env files to *previous* version
|
|
||||||
// Now that the latest tag is gone, getCurrentVersion will return the previous one
|
|
||||||
const previousVersion = await this.s4.getCurrentVersion() || '0.0.0';
|
|
||||||
|
|
||||||
if (previousVersion) {
|
|
||||||
console.log(`Reverting environment to previous version: ${color.dim(previousVersion)}`);
|
|
||||||
const updatedFiles = this.updateEnvFiles(previousVersion);
|
|
||||||
if (updatedFiles.length > 0) {
|
|
||||||
console.log(`${color.green('✔')} Updated ${updatedFiles.join(', ')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(color.dim('\nNote: If you already pushed the tag, you must verify deletion on remote:'));
|
|
||||||
console.log(` ${color.cyan(`git push --delete origin ${currentVersion}`)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
showInfo(versionStr) {
|
|
||||||
const parsed = S4Versioning.parse(versionStr);
|
|
||||||
if (!parsed) {
|
|
||||||
console.log(color.red('Invalid S4 version format'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format date/time
|
|
||||||
const y = parsed.date.substring(0, 4);
|
|
||||||
const m = parsed.date.substring(4, 6);
|
|
||||||
const d = parsed.date.substring(6, 8);
|
|
||||||
const H = parsed.time.substring(0, 2);
|
|
||||||
const M = parsed.time.substring(2, 4);
|
|
||||||
const dateObj = new Date(`${y}-${m}-${d}T${H}:${M}:00`);
|
|
||||||
|
|
||||||
// 12-hour format logic
|
|
||||||
let hour = parseInt(H);
|
|
||||||
const ampm = hour >= 12 ? 'PM' : 'AM';
|
|
||||||
hour = hour % 12;
|
|
||||||
hour = hour ? hour : 12; // the hour '0' should be '12'
|
|
||||||
const time12 = `${hour}:${M} ${ampm}`;
|
|
||||||
|
|
||||||
console.log(color.bold('\nS4 Version Analysis:'));
|
|
||||||
console.log(`Major: ${parsed.major}`);
|
|
||||||
console.log(`Minor: ${parsed.minor}`);
|
|
||||||
console.log(`Patch: ${parsed.patch}`);
|
|
||||||
console.log(`Stage: ${color.magenta(parsed.stage)}`);
|
|
||||||
console.log(`Build: ${parsed.build}`);
|
|
||||||
console.log(`Date: ${dateObj.toDateString()}`);
|
|
||||||
console.log(`Time: ${time12} (${H}:${M})`);
|
|
||||||
console.log(`Git: ${parsed.branch} (${parsed.commit})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { VersionCommand };
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
const { color } = require('../utils/colors');
|
|
||||||
const os = require('os');
|
|
||||||
|
|
||||||
class WhoAmICommand {
|
|
||||||
constructor(git) {
|
|
||||||
this.git = git;
|
|
||||||
}
|
|
||||||
|
|
||||||
async run() {
|
|
||||||
const pkg = require('../../../package.json');
|
|
||||||
|
|
||||||
// Get SSOT version from S4 (Tags)
|
|
||||||
const { S4Versioning } = require('../utils/s4');
|
|
||||||
const s4 = new S4Versioning(this.git);
|
|
||||||
const version = (await s4.getCurrentVersion()) || pkg.version;
|
|
||||||
|
|
||||||
const user = os.userInfo().username;
|
|
||||||
const hostname = os.hostname();
|
|
||||||
const platform = os.platform();
|
|
||||||
const arch = os.arch();
|
|
||||||
|
|
||||||
let gitUser = 'unknown';
|
|
||||||
let gitEmail = 'unknown';
|
|
||||||
try {
|
|
||||||
gitUser = (await this.git.raw(['config', 'user.name'])).trim();
|
|
||||||
gitEmail = (await this.git.raw(['config', 'user.email'])).trim();
|
|
||||||
} catch (e) { }
|
|
||||||
|
|
||||||
console.log(color.bold('\n⚡ GIMS SYSTEM STATUS ⚡\n'));
|
|
||||||
|
|
||||||
console.log(`${color.green('SYSTEM_IDENTITY')}`);
|
|
||||||
console.log(`├─ Host: ${color.cyan(hostname)}`);
|
|
||||||
console.log(`├─ User: ${color.cyan(user)}`);
|
|
||||||
console.log(`└─ OS: ${platform} (${arch})`);
|
|
||||||
|
|
||||||
console.log(`\n${color.green('GIT_OPERATOR')}`);
|
|
||||||
console.log(`├─ Name: ${color.cyan(gitUser)}`);
|
|
||||||
console.log(`└─ Email: ${color.cyan(gitEmail)}`);
|
|
||||||
|
|
||||||
console.log(`\n${color.green('TOOL_METRICS')}`);
|
|
||||||
console.log(`├─ Version: ${color.magenta(version)}`);
|
|
||||||
console.log(`├─ Engine: Node ${process.version}`);
|
|
||||||
console.log(`└─ Author: ${pkg.author}`);
|
|
||||||
|
|
||||||
console.log('\n' + color.dim('----------------------------------------'));
|
|
||||||
console.log(color.dim(`Initialized at: ${new Date().toISOString()}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { WhoAmICommand };
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const { color } = require('../utils/colors');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enhanced configuration management with validation and setup wizard
|
|
||||||
*/
|
|
||||||
class ConfigManager {
|
|
||||||
constructor() {
|
|
||||||
this.configPaths = [
|
|
||||||
path.join(process.cwd(), '.gimsrc'),
|
|
||||||
path.join(process.env.HOME || process.cwd(), '.gimsrc'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaults() {
|
|
||||||
return {
|
|
||||||
provider: process.env.GIMS_PROVIDER || 'auto',
|
|
||||||
model: process.env.GIMS_MODEL || '',
|
|
||||||
conventional: !!(process.env.GIMS_CONVENTIONAL === '1'),
|
|
||||||
copy: process.env.GIMS_COPY !== '0',
|
|
||||||
autoStage: process.env.GIMS_AUTO_STAGE === '1',
|
|
||||||
maxDiffSize: parseInt(process.env.GIMS_MAX_DIFF_SIZE) || 100000,
|
|
||||||
cacheEnabled: process.env.GIMS_CACHE !== '0',
|
|
||||||
progressIndicators: process.env.GIMS_PROGRESS !== '0'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
load() {
|
|
||||||
const defaults = this.getDefaults();
|
|
||||||
|
|
||||||
for (const configPath of this.configPaths) {
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(configPath)) {
|
|
||||||
const content = fs.readFileSync(configPath, 'utf8');
|
|
||||||
const config = JSON.parse(content);
|
|
||||||
return { ...defaults, ...config, _source: configPath };
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(color.yellow(`Warning: Invalid config file ${configPath}: ${error.message}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...defaults, _source: 'defaults' };
|
|
||||||
}
|
|
||||||
|
|
||||||
save(config, global = false) {
|
|
||||||
const configPath = global
|
|
||||||
? path.join(process.env.HOME || process.cwd(), '.gimsrc')
|
|
||||||
: path.join(process.cwd(), '.gimsrc');
|
|
||||||
|
|
||||||
// Remove internal properties
|
|
||||||
const { _source, ...cleanConfig } = config;
|
|
||||||
|
|
||||||
try {
|
|
||||||
fs.writeFileSync(configPath, JSON.stringify(cleanConfig, null, 2));
|
|
||||||
return configPath;
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to save config: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set(key, value, global = false) {
|
|
||||||
const config = this.load();
|
|
||||||
|
|
||||||
// Validate key
|
|
||||||
const validKeys = Object.keys(this.getDefaults());
|
|
||||||
if (!validKeys.includes(key)) {
|
|
||||||
throw new Error(`Invalid config key: ${key}. Valid keys: ${validKeys.join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type conversion
|
|
||||||
if (typeof this.getDefaults()[key] === 'boolean') {
|
|
||||||
value = value === 'true' || value === '1';
|
|
||||||
} else if (typeof this.getDefaults()[key] === 'number') {
|
|
||||||
value = parseInt(value);
|
|
||||||
if (isNaN(value)) {
|
|
||||||
throw new Error(`Invalid number value for ${key}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config[key] = value;
|
|
||||||
const savedPath = this.save(config, global);
|
|
||||||
return { key, value, savedPath };
|
|
||||||
}
|
|
||||||
|
|
||||||
get(key) {
|
|
||||||
const config = this.load();
|
|
||||||
if (key) {
|
|
||||||
return config[key];
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
detectProjectType() {
|
|
||||||
const cwd = process.cwd();
|
|
||||||
|
|
||||||
if (fs.existsSync(path.join(cwd, 'package.json'))) {
|
|
||||||
try {
|
|
||||||
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
||||||
if (pkg.dependencies?.react || pkg.devDependencies?.react) return 'react';
|
|
||||||
if (pkg.dependencies?.vue || pkg.devDependencies?.vue) return 'vue';
|
|
||||||
if (pkg.dependencies?.angular || pkg.devDependencies?.angular) return 'angular';
|
|
||||||
if (pkg.dependencies?.express || pkg.devDependencies?.express) return 'express';
|
|
||||||
return 'node';
|
|
||||||
} catch (e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fs.existsSync(path.join(cwd, 'requirements.txt')) ||
|
|
||||||
fs.existsSync(path.join(cwd, 'pyproject.toml'))) return 'python';
|
|
||||||
if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) return 'rust';
|
|
||||||
if (fs.existsSync(path.join(cwd, 'go.mod'))) return 'go';
|
|
||||||
if (fs.existsSync(path.join(cwd, 'pom.xml'))) return 'java';
|
|
||||||
|
|
||||||
return 'generic';
|
|
||||||
}
|
|
||||||
|
|
||||||
getProjectCommitStyle(projectType) {
|
|
||||||
const styles = {
|
|
||||||
react: { conventional: true, types: ['feat', 'fix', 'style', 'refactor', 'test'] },
|
|
||||||
vue: { conventional: true, types: ['feat', 'fix', 'style', 'refactor', 'test'] },
|
|
||||||
angular: { conventional: true, types: ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore'] },
|
|
||||||
node: { conventional: true, types: ['feat', 'fix', 'perf', 'refactor', 'test', 'chore'] },
|
|
||||||
python: { conventional: false, style: 'descriptive' },
|
|
||||||
generic: { conventional: false, style: 'simple' }
|
|
||||||
};
|
|
||||||
|
|
||||||
return styles[projectType] || styles.generic;
|
|
||||||
}
|
|
||||||
|
|
||||||
async runSetupWizard() {
|
|
||||||
const readline = require('readline');
|
|
||||||
const rl = readline.createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
output: process.stdout
|
|
||||||
});
|
|
||||||
|
|
||||||
const question = (prompt) => new Promise(resolve => {
|
|
||||||
rl.question(prompt, answer => {
|
|
||||||
resolve(answer.trim());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(color.bold('\n🚀 GIMS Setup Wizard\n'));
|
|
||||||
|
|
||||||
// Detect project
|
|
||||||
const projectType = this.detectProjectType();
|
|
||||||
const projectStyle = this.getProjectCommitStyle(projectType);
|
|
||||||
|
|
||||||
console.log(`Detected project type: ${color.cyan(projectType)}`);
|
|
||||||
|
|
||||||
// Provider setup
|
|
||||||
console.log('\n📡 AI Provider Setup:');
|
|
||||||
const hasOpenAI = !!process.env.OPENAI_API_KEY;
|
|
||||||
const hasGemini = !!process.env.GEMINI_API_KEY;
|
|
||||||
const hasGroq = !!process.env.GROQ_API_KEY;
|
|
||||||
|
|
||||||
if (!hasOpenAI && !hasGemini && !hasGroq) {
|
|
||||||
console.log(color.yellow('No AI providers detected. GIMS will use local heuristics.'));
|
|
||||||
console.log('\nTo enable AI features, run:');
|
|
||||||
console.log(` ${color.cyan('g setup --api-key gemini')} # Recommended: Fast & free`);
|
|
||||||
console.log(` ${color.cyan('g setup --api-key openai')} # High quality`);
|
|
||||||
console.log(` ${color.cyan('g setup --api-key groq')} # Ultra fast`);
|
|
||||||
console.log('\nOr set environment variables manually:');
|
|
||||||
console.log(' - OPENAI_API_KEY (gpt-5)');
|
|
||||||
console.log(' - GEMINI_API_KEY (gemini-2.5-flash)');
|
|
||||||
console.log(' - GROQ_API_KEY (groq/compound)');
|
|
||||||
} else {
|
|
||||||
console.log('Available providers with default models:');
|
|
||||||
if (hasGemini) console.log(` ${color.green('✓')} Google Gemini (gemini-2.5-flash)`);
|
|
||||||
if (hasOpenAI) console.log(` ${color.green('✓')} OpenAI (gpt-5)`);
|
|
||||||
if (hasGroq) console.log(` ${color.green('✓')} Groq (groq/compound)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 };
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
const { color } = require('../utils/colors');
|
|
||||||
const { Intelligence } = require('../utils/intelligence');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enhanced git analysis and insights
|
|
||||||
*/
|
|
||||||
class GitAnalyzer {
|
|
||||||
constructor(git) {
|
|
||||||
this.git = git;
|
|
||||||
this.intelligence = new Intelligence(git);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getEnhancedStatus() {
|
|
||||||
try {
|
|
||||||
const status = await this.git.status();
|
|
||||||
const insights = await this.generateStatusInsights(status);
|
|
||||||
const sessionStats = await this.intelligence.getSessionStats();
|
|
||||||
const branchContext = await this.intelligence.detectBranchContext();
|
|
||||||
|
|
||||||
return {
|
|
||||||
...status,
|
|
||||||
insights,
|
|
||||||
summary: this.generateStatusSummary(status),
|
|
||||||
sessionStats,
|
|
||||||
branchContext,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to get git status: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generateStatusSummary(status) {
|
|
||||||
const files = Array.isArray(status.files) ? status.files : [];
|
|
||||||
const staged = Array.isArray(status.staged) ? status.staged : [];
|
|
||||||
const modified = Array.isArray(status.modified) ? status.modified : [];
|
|
||||||
const created = Array.isArray(status.created) ? status.created : [];
|
|
||||||
const deleted = Array.isArray(status.deleted) ? status.deleted : [];
|
|
||||||
const untracked = Array.isArray(status.not_added) ? status.not_added : [];
|
|
||||||
|
|
||||||
if (files.length === 0) return 'Working tree clean';
|
|
||||||
|
|
||||||
const parts = [];
|
|
||||||
if (staged.length > 0) parts.push(`${staged.length} staged`);
|
|
||||||
if (modified.length > 0) parts.push(`${modified.length} modified`);
|
|
||||||
if (created.length > 0) parts.push(`${created.length} new`);
|
|
||||||
if (deleted.length > 0) parts.push(`${deleted.length} deleted`);
|
|
||||||
if (untracked.length > 0) parts.push(`${untracked.length} untracked`);
|
|
||||||
|
|
||||||
return parts.join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
async generateStatusInsights(status) {
|
|
||||||
const insights = [];
|
|
||||||
|
|
||||||
// Ensure arrays exist and have proper methods
|
|
||||||
const modified = Array.isArray(status.modified) ? status.modified : [];
|
|
||||||
const created = Array.isArray(status.created) ? status.created : [];
|
|
||||||
const deleted = Array.isArray(status.deleted) ? status.deleted : [];
|
|
||||||
const files = Array.isArray(status.files) ? status.files : [];
|
|
||||||
const staged = Array.isArray(status.staged) ? status.staged : [];
|
|
||||||
|
|
||||||
// Check for common patterns
|
|
||||||
if (modified.some(f => String(f).includes('package.json'))) {
|
|
||||||
insights.push('📦 Dependencies may have changed - consider updating package-lock.json');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (created.some(f => String(f).includes('.env'))) {
|
|
||||||
insights.push('🔐 New environment file detected - ensure it\'s in .gitignore');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modified.some(f => String(f).includes('README'))) {
|
|
||||||
insights.push('📚 Documentation updated - good practice!');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deleted.length > created.length + modified.length) {
|
|
||||||
insights.push('🧹 Cleanup operation detected - removing more than adding');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (files.length > 20) {
|
|
||||||
insights.push('📊 Large changeset - consider using `g split` to break into smaller commits');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for test files
|
|
||||||
const testFiles = files.filter(f => {
|
|
||||||
const fileName = String(f);
|
|
||||||
return fileName.includes('.test.') || fileName.includes('.spec.') || fileName.includes('__tests__');
|
|
||||||
});
|
|
||||||
if (testFiles.length > 0) {
|
|
||||||
insights.push('🧪 Test files modified - great for code quality!');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for config files
|
|
||||||
const configFiles = files.filter(f => {
|
|
||||||
const fileName = String(f);
|
|
||||||
return fileName.includes('config') || fileName.includes('.json') || fileName.includes('.yml') || fileName.includes('.yaml');
|
|
||||||
});
|
|
||||||
if (configFiles.length > 0) {
|
|
||||||
insights.push('⚙️ Configuration changes detected');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suggest staging if nothing staged
|
|
||||||
if (staged.length === 0 && files.length > 0) {
|
|
||||||
insights.push('💡 Nothing staged yet - use `g o --all` to stage and commit everything');
|
|
||||||
}
|
|
||||||
|
|
||||||
return insights;
|
|
||||||
}
|
|
||||||
|
|
||||||
async analyzeCommitHistory(limit = 10) {
|
|
||||||
try {
|
|
||||||
const log = await this.git.log({ maxCount: limit });
|
|
||||||
const commits = log.all;
|
|
||||||
|
|
||||||
if (commits.length === 0) {
|
|
||||||
return { totalCommits: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
const analysis = {
|
|
||||||
totalCommits: commits.length,
|
|
||||||
authors: [...new Set(commits.map(c => c.author_name))],
|
|
||||||
averageMessageLength: commits.reduce((sum, c) => sum + c.message.length, 0) / commits.length,
|
|
||||||
conventionalCommits: commits.filter(c => /^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?:/.test(c.message)).length,
|
|
||||||
recentActivity: this.analyzeRecentActivity(commits)
|
|
||||||
};
|
|
||||||
|
|
||||||
return analysis;
|
|
||||||
} catch (error) {
|
|
||||||
return { error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
analyzeRecentActivity(commits) {
|
|
||||||
const now = new Date();
|
|
||||||
const oneDayAgo = new Date(now - 24 * 60 * 60 * 1000);
|
|
||||||
const oneWeekAgo = new Date(now - 7 * 24 * 60 * 60 * 1000);
|
|
||||||
|
|
||||||
const recentCommits = commits.filter(c => new Date(c.date) > oneDayAgo);
|
|
||||||
const weeklyCommits = commits.filter(c => new Date(c.date) > oneWeekAgo);
|
|
||||||
|
|
||||||
return {
|
|
||||||
last24h: recentCommits.length,
|
|
||||||
lastWeek: weeklyCommits.length,
|
|
||||||
frequency: weeklyCommits.length > 0 ? 'active' : 'quiet'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getChangeComplexity(diff) {
|
|
||||||
const lines = diff.split('\n');
|
|
||||||
const additions = lines.filter(l => l.startsWith('+')).length;
|
|
||||||
const deletions = lines.filter(l => l.startsWith('-')).length;
|
|
||||||
const files = (diff.match(/diff --git/g) || []).length;
|
|
||||||
|
|
||||||
let complexity = 'simple';
|
|
||||||
let emoji = '🟢';
|
|
||||||
if (files > 10 || additions + deletions > 500) {
|
|
||||||
complexity = 'complex';
|
|
||||||
emoji = '🔴';
|
|
||||||
} else if (files > 5 || additions + deletions > 100) {
|
|
||||||
complexity = 'moderate';
|
|
||||||
emoji = '🟡';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
complexity,
|
|
||||||
emoji,
|
|
||||||
files,
|
|
||||||
additions,
|
|
||||||
deletions,
|
|
||||||
total: additions + deletions
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
formatStatusOutput(enhancedStatus) {
|
|
||||||
const {
|
|
||||||
files = [],
|
|
||||||
staged = [],
|
|
||||||
modified = [],
|
|
||||||
created = [],
|
|
||||||
deleted = [],
|
|
||||||
not_added = [],
|
|
||||||
insights = [],
|
|
||||||
summary = 'Unknown status',
|
|
||||||
sessionStats = {},
|
|
||||||
branchContext = {},
|
|
||||||
} = enhancedStatus;
|
|
||||||
|
|
||||||
let output = '';
|
|
||||||
|
|
||||||
// Header with branch info
|
|
||||||
output += `${color.bold('Git Status')}`;
|
|
||||||
if (branchContext.branch) {
|
|
||||||
output += ` ${color.dim('on')} ${color.cyan(branchContext.branch)}`;
|
|
||||||
if (branchContext.type) {
|
|
||||||
output += ` ${color.dim(`(${branchContext.type})`)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output += '\n';
|
|
||||||
output += `${color.dim(summary)}\n`;
|
|
||||||
|
|
||||||
// Session stats
|
|
||||||
if (sessionStats.timeSinceLastCommit) {
|
|
||||||
output += `${color.dim(`Last commit: ${sessionStats.timeSinceLastCommit}`)}`;
|
|
||||||
if (sessionStats.commitsToday > 0) {
|
|
||||||
output += `${color.dim(` • ${sessionStats.commitsToday} commit${sessionStats.commitsToday > 1 ? 's' : ''} today`)}`;
|
|
||||||
}
|
|
||||||
output += '\n';
|
|
||||||
}
|
|
||||||
output += '\n';
|
|
||||||
|
|
||||||
// Staged changes
|
|
||||||
if (staged.length > 0) {
|
|
||||||
output += `${color.green('Staged for commit:')}\n`;
|
|
||||||
staged.forEach(file => {
|
|
||||||
const emoji = Intelligence.getFileEmoji(file);
|
|
||||||
output += ` ${color.green('+')} ${emoji} ${file}\n`;
|
|
||||||
});
|
|
||||||
output += '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modified files
|
|
||||||
if (modified.length > 0) {
|
|
||||||
output += `${color.yellow('Modified (not staged):')}\n`;
|
|
||||||
modified.forEach(file => {
|
|
||||||
const emoji = Intelligence.getFileEmoji(file);
|
|
||||||
output += ` ${color.yellow('M')} ${emoji} ${file}\n`;
|
|
||||||
});
|
|
||||||
output += '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
// New files
|
|
||||||
if (created.length > 0) {
|
|
||||||
output += `${color.cyan('New files:')}\n`;
|
|
||||||
created.forEach(file => {
|
|
||||||
const emoji = Intelligence.getFileEmoji(file);
|
|
||||||
output += ` ${color.cyan('N')} ${emoji} ${file}\n`;
|
|
||||||
});
|
|
||||||
output += '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deleted files
|
|
||||||
if (deleted.length > 0) {
|
|
||||||
output += `${color.red('Deleted:')}\n`;
|
|
||||||
deleted.forEach(file => {
|
|
||||||
const emoji = Intelligence.getFileEmoji(file);
|
|
||||||
output += ` ${color.red('D')} ${emoji} ${file}\n`;
|
|
||||||
});
|
|
||||||
output += '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Untracked files
|
|
||||||
if (not_added.length > 0) {
|
|
||||||
output += `${color.dim('Untracked files:')}\n`;
|
|
||||||
not_added.slice(0, 10).forEach(file => {
|
|
||||||
const emoji = Intelligence.getFileEmoji(file);
|
|
||||||
output += ` ${color.dim('?')} ${emoji} ${file}\n`;
|
|
||||||
});
|
|
||||||
if (not_added.length > 10) {
|
|
||||||
output += ` ${color.dim(`... and ${not_added.length - 10} more`)}\n`;
|
|
||||||
}
|
|
||||||
output += '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
// AI Insights
|
|
||||||
if (insights.length > 0) {
|
|
||||||
output += `${color.cyan('💡 Insights:')}\n`;
|
|
||||||
insights.forEach(insight => {
|
|
||||||
output += ` ${insight}\n`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get today's commits formatted nicely
|
|
||||||
*/
|
|
||||||
async getTodayCommits() {
|
|
||||||
try {
|
|
||||||
const today = new Date();
|
|
||||||
today.setHours(0, 0, 0, 0);
|
|
||||||
const log = await this.git.log({ '--since': today.toISOString() });
|
|
||||||
return log.all;
|
|
||||||
} catch (error) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format commit for display
|
|
||||||
*/
|
|
||||||
formatCommit(commit, index) {
|
|
||||||
const hash = color.yellow(commit.hash.substring(0, 7));
|
|
||||||
const message = commit.message.split('\n')[0];
|
|
||||||
const time = new Date(commit.date).toLocaleTimeString('en-US', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
return ` ${color.dim(`${index + 1}.`)} ${hash} ${message} ${color.dim(`(${time})`)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { GitAnalyzer };
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/**
|
|
||||||
* ANSI color utilities without external dependencies
|
|
||||||
*/
|
|
||||||
const color = {
|
|
||||||
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
||||||
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
||||||
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
||||||
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
||||||
blue: (s) => `\x1b[34m${s}\x1b[0m`,
|
|
||||||
magenta: (s) => `\x1b[35m${s}\x1b[0m`,
|
|
||||||
white: (s) => `\x1b[37m${s}\x1b[0m`,
|
|
||||||
gray: (s) => `\x1b[90m${s}\x1b[0m`,
|
|
||||||
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
||||||
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
||||||
italic: (s) => `\x1b[3m${s}\x1b[0m`,
|
|
||||||
underline: (s) => `\x1b[4m${s}\x1b[0m`,
|
|
||||||
// Background colors
|
|
||||||
bgGreen: (s) => `\x1b[42m\x1b[30m${s}\x1b[0m`,
|
|
||||||
bgYellow: (s) => `\x1b[43m\x1b[30m${s}\x1b[0m`,
|
|
||||||
bgRed: (s) => `\x1b[41m\x1b[37m${s}\x1b[0m`,
|
|
||||||
bgCyan: (s) => `\x1b[46m\x1b[30m${s}\x1b[0m`,
|
|
||||||
bgBlue: (s) => `\x1b[44m\x1b[37m${s}\x1b[0m`,
|
|
||||||
// Compound styles
|
|
||||||
success: (s) => `\x1b[32m✓\x1b[0m ${s}`,
|
|
||||||
warning: (s) => `\x1b[33m⚠\x1b[0m ${s}`,
|
|
||||||
error: (s) => `\x1b[31m✗\x1b[0m ${s}`,
|
|
||||||
info: (s) => `\x1b[36mℹ\x1b[0m ${s}`,
|
|
||||||
reset: '\x1b[0m'
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = { color };
|
|
||||||
@@ -1,421 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Intelligence utilities for smart git operations
|
|
||||||
*/
|
|
||||||
class Intelligence {
|
|
||||||
constructor(git) {
|
|
||||||
this.git = git;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get file type emoji based on file extension
|
|
||||||
*/
|
|
||||||
static getFileEmoji(filename) {
|
|
||||||
const ext = path.extname(filename).toLowerCase();
|
|
||||||
const name = path.basename(filename).toLowerCase();
|
|
||||||
|
|
||||||
// Special files
|
|
||||||
if (name === 'package.json' || name === 'package-lock.json') return '📦';
|
|
||||||
if (name === 'readme.md' || name === 'readme') return '📚';
|
|
||||||
if (name === '.gitignore') return '🙈';
|
|
||||||
if (name === '.env' || name.startsWith('.env.')) return '🔐';
|
|
||||||
if (name === 'dockerfile' || name === 'docker-compose.yml') return '🐳';
|
|
||||||
if (name.includes('config') || name.includes('rc')) return '⚙️';
|
|
||||||
if (name === 'license' || name === 'license.md') return '📜';
|
|
||||||
|
|
||||||
// Test files
|
|
||||||
if (filename.includes('.test.') || filename.includes('.spec.') || filename.includes('__tests__')) return '🧪';
|
|
||||||
|
|
||||||
// By extension
|
|
||||||
const emojiMap = {
|
|
||||||
'.js': '📄', '.jsx': '⚛️', '.ts': '📘', '.tsx': '⚛️',
|
|
||||||
'.css': '🎨', '.scss': '🎨', '.sass': '🎨', '.less': '🎨',
|
|
||||||
'.html': '🌐', '.htm': '🌐',
|
|
||||||
'.json': '📋', '.yaml': '📋', '.yml': '📋', '.toml': '📋',
|
|
||||||
'.md': '📝', '.mdx': '📝', '.txt': '📄',
|
|
||||||
'.py': '🐍', '.rb': '💎', '.go': '🔷', '.rs': '🦀', '.java': '☕',
|
|
||||||
'.sh': '🐚', '.bash': '🐚', '.zsh': '🐚',
|
|
||||||
'.sql': '🗃️', '.graphql': '🔗', '.gql': '🔗',
|
|
||||||
'.png': '🖼️', '.jpg': '🖼️', '.jpeg': '🖼️', '.gif': '🖼️', '.svg': '🎯',
|
|
||||||
'.mp3': '🎵', '.wav': '🎵', '.mp4': '🎬', '.mov': '🎬',
|
|
||||||
'.zip': '📦', '.tar': '📦', '.gz': '📦',
|
|
||||||
'.lock': '🔒',
|
|
||||||
};
|
|
||||||
|
|
||||||
return emojiMap[ext] || '📄';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Analyze user's commit patterns from history
|
|
||||||
*/
|
|
||||||
async analyzeCommitPatterns(limit = 50) {
|
|
||||||
try {
|
|
||||||
const log = await this.git.log({ maxCount: limit });
|
|
||||||
const commits = log.all;
|
|
||||||
|
|
||||||
if (commits.length === 0) {
|
|
||||||
return { hasHistory: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Analyze patterns
|
|
||||||
const conventionalPattern = /^(feat|fix|docs|style|refactor|test|chore|perf|build|ci|revert)(\(.+\))?:/i;
|
|
||||||
const conventionalCommits = commits.filter(c => conventionalPattern.test(c.message));
|
|
||||||
|
|
||||||
// Calculate average message length
|
|
||||||
const avgLength = Math.round(commits.reduce((sum, c) => sum + c.message.split('\n')[0].length, 0) / commits.length);
|
|
||||||
|
|
||||||
// Detect common scopes
|
|
||||||
const scopes = {};
|
|
||||||
commits.forEach(c => {
|
|
||||||
const match = c.message.match(/^\w+\(([^)]+)\)/);
|
|
||||||
if (match) {
|
|
||||||
scopes[match[1]] = (scopes[match[1]] || 0) + 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const topScopes = Object.entries(scopes)
|
|
||||||
.sort((a, b) => b[1] - a[1])
|
|
||||||
.slice(0, 5)
|
|
||||||
.map(([scope]) => scope);
|
|
||||||
|
|
||||||
// Detect if user uses emojis
|
|
||||||
const emojiPattern = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]/u;
|
|
||||||
const usesEmojis = commits.some(c => emojiPattern.test(c.message));
|
|
||||||
|
|
||||||
// Detect commit message style (imperative vs past tense)
|
|
||||||
const imperativeWords = ['add', 'fix', 'update', 'remove', 'change', 'implement', 'refactor'];
|
|
||||||
const pastTenseWords = ['added', 'fixed', 'updated', 'removed', 'changed', 'implemented', 'refactored'];
|
|
||||||
|
|
||||||
let imperativeCount = 0;
|
|
||||||
let pastTenseCount = 0;
|
|
||||||
commits.forEach(c => {
|
|
||||||
const firstWord = c.message.split(/[\s(:]/)[0].toLowerCase();
|
|
||||||
if (imperativeWords.includes(firstWord)) imperativeCount++;
|
|
||||||
if (pastTenseWords.includes(firstWord)) pastTenseCount++;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasHistory: true,
|
|
||||||
totalAnalyzed: commits.length,
|
|
||||||
conventionalRatio: (conventionalCommits.length / commits.length * 100).toFixed(0),
|
|
||||||
usesConventional: conventionalCommits.length > commits.length * 0.5,
|
|
||||||
avgMessageLength: avgLength,
|
|
||||||
topScopes,
|
|
||||||
usesEmojis,
|
|
||||||
style: imperativeCount >= pastTenseCount ? 'imperative' : 'past-tense',
|
|
||||||
authors: [...new Set(commits.map(c => c.author_name))],
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return { hasHistory: false, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect semantic meaning of changes
|
|
||||||
*/
|
|
||||||
async detectSemanticChanges(diff) {
|
|
||||||
const changes = {
|
|
||||||
type: 'misc',
|
|
||||||
labels: [],
|
|
||||||
suggestions: [],
|
|
||||||
breakingChange: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!diff || !diff.trim()) return changes;
|
|
||||||
|
|
||||||
const lines = diff.split('\n');
|
|
||||||
const addedLines = lines.filter(l => l.startsWith('+') && !l.startsWith('+++'));
|
|
||||||
const removedLines = lines.filter(l => l.startsWith('-') && !l.startsWith('---'));
|
|
||||||
|
|
||||||
// Detect breaking changes
|
|
||||||
if (removedLines.some(l => l.includes('export ') && l.includes('function ')) ||
|
|
||||||
removedLines.some(l => l.includes('module.exports'))) {
|
|
||||||
changes.labels.push('⚠️ Potential breaking change');
|
|
||||||
changes.breakingChange = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect new features
|
|
||||||
if (addedLines.some(l => l.includes('export ') && l.includes('function ')) ||
|
|
||||||
addedLines.some(l => l.includes('class ')) ||
|
|
||||||
addedLines.some(l => l.includes('async function '))) {
|
|
||||||
changes.labels.push('✨ New functionality');
|
|
||||||
changes.type = 'feat';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect bug fixes
|
|
||||||
if (addedLines.some(l => l.includes('catch') || l.includes('try {')) ||
|
|
||||||
addedLines.some(l => l.includes('|| null') || l.includes('?? ') || l.includes('?.'))) {
|
|
||||||
changes.labels.push('🐛 Error handling');
|
|
||||||
if (changes.type === 'misc') changes.type = 'fix';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect refactoring
|
|
||||||
if (addedLines.length > 20 && removedLines.length > 20 &&
|
|
||||||
Math.abs(addedLines.length - removedLines.length) < 10) {
|
|
||||||
changes.labels.push('♻️ Refactoring');
|
|
||||||
if (changes.type === 'misc') changes.type = 'refactor';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect documentation
|
|
||||||
if (addedLines.some(l => l.includes('/**') || l.includes('* @') || l.includes('// '))) {
|
|
||||||
changes.labels.push('📝 Documentation');
|
|
||||||
if (changes.type === 'misc') changes.type = 'docs';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect test changes
|
|
||||||
if (diff.includes('.test.') || diff.includes('.spec.') ||
|
|
||||||
addedLines.some(l => l.includes('describe(') || l.includes('it(') || l.includes('test('))) {
|
|
||||||
changes.labels.push('🧪 Tests');
|
|
||||||
if (changes.type === 'misc') changes.type = 'test';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect dependency changes
|
|
||||||
if (diff.includes('package.json') && (diff.includes('"dependencies"') || diff.includes('"devDependencies"'))) {
|
|
||||||
changes.labels.push('📦 Dependencies');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect style/formatting
|
|
||||||
if (addedLines.every(l => l.trim().length < 3 || l.includes(' ') || l === '+')) {
|
|
||||||
changes.labels.push('🎨 Formatting');
|
|
||||||
if (changes.type === 'misc') changes.type = 'style';
|
|
||||||
}
|
|
||||||
|
|
||||||
return changes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Suggest how to split a large changeset
|
|
||||||
*/
|
|
||||||
async suggestCommitSplit(status) {
|
|
||||||
const files = status.files || [];
|
|
||||||
if (files.length < 5) return null;
|
|
||||||
|
|
||||||
const groups = {
|
|
||||||
tests: [],
|
|
||||||
config: [],
|
|
||||||
docs: [],
|
|
||||||
styles: [],
|
|
||||||
core: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
files.forEach(file => {
|
|
||||||
const f = typeof file === 'string' ? file : file.path;
|
|
||||||
if (!f) return;
|
|
||||||
|
|
||||||
if (f.includes('.test.') || f.includes('.spec.') || f.includes('__tests__')) {
|
|
||||||
groups.tests.push(f);
|
|
||||||
} else if (f.includes('config') || f.endsWith('.json') || f.endsWith('.yml') || f.endsWith('.yaml') || f.includes('rc')) {
|
|
||||||
groups.config.push(f);
|
|
||||||
} else if (f.endsWith('.md') || f.includes('docs/') || f.includes('README')) {
|
|
||||||
groups.docs.push(f);
|
|
||||||
} else if (f.endsWith('.css') || f.endsWith('.scss') || f.endsWith('.sass')) {
|
|
||||||
groups.styles.push(f);
|
|
||||||
} else {
|
|
||||||
groups.core.push(f);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const suggestions = [];
|
|
||||||
if (groups.tests.length > 0) suggestions.push({ type: 'test', files: groups.tests, message: 'test: add/update tests' });
|
|
||||||
if (groups.config.length > 0) suggestions.push({ type: 'chore', files: groups.config, message: 'chore: update configuration' });
|
|
||||||
if (groups.docs.length > 0) suggestions.push({ type: 'docs', files: groups.docs, message: 'docs: update documentation' });
|
|
||||||
if (groups.styles.length > 0) suggestions.push({ type: 'style', files: groups.styles, message: 'style: update styles' });
|
|
||||||
if (groups.core.length > 0) suggestions.push({ type: 'feat', files: groups.core, message: 'feat: update core functionality' });
|
|
||||||
|
|
||||||
return suggestions.length > 1 ? suggestions : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get session statistics
|
|
||||||
*/
|
|
||||||
async getSessionStats() {
|
|
||||||
try {
|
|
||||||
const log = await this.git.log({ maxCount: 1 });
|
|
||||||
const lastCommit = log.all[0];
|
|
||||||
|
|
||||||
let timeSinceLastCommit = 'No commits yet';
|
|
||||||
let lastCommitMessage = null;
|
|
||||||
|
|
||||||
if (lastCommit) {
|
|
||||||
const lastCommitDate = new Date(lastCommit.date);
|
|
||||||
const now = new Date();
|
|
||||||
const diffMs = now - lastCommitDate;
|
|
||||||
const diffMins = Math.floor(diffMs / 60000);
|
|
||||||
const diffHours = Math.floor(diffMins / 60);
|
|
||||||
const diffDays = Math.floor(diffHours / 24);
|
|
||||||
|
|
||||||
if (diffDays > 0) {
|
|
||||||
timeSinceLastCommit = `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
|
|
||||||
} else if (diffHours > 0) {
|
|
||||||
timeSinceLastCommit = `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
|
||||||
} else if (diffMins > 0) {
|
|
||||||
timeSinceLastCommit = `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
|
|
||||||
} else {
|
|
||||||
timeSinceLastCommit = 'Just now';
|
|
||||||
}
|
|
||||||
|
|
||||||
lastCommitMessage = lastCommit.message.split('\n')[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get today's commits
|
|
||||||
const today = new Date();
|
|
||||||
today.setHours(0, 0, 0, 0);
|
|
||||||
const todayLog = await this.git.log({ '--since': today.toISOString() });
|
|
||||||
const todayCommits = todayLog.all.length;
|
|
||||||
|
|
||||||
return {
|
|
||||||
timeSinceLastCommit,
|
|
||||||
lastCommitMessage,
|
|
||||||
commitsToday: todayCommits,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
timeSinceLastCommit: 'Unknown',
|
|
||||||
lastCommitMessage: null,
|
|
||||||
commitsToday: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect branch context for smarter commit messages
|
|
||||||
*/
|
|
||||||
async detectBranchContext() {
|
|
||||||
try {
|
|
||||||
const branch = await this.git.branch();
|
|
||||||
const currentBranch = branch.current;
|
|
||||||
|
|
||||||
// Common patterns: feat/xyz, fix/xyz, feature/xyz, bugfix/xyz, hotfix/xyz
|
|
||||||
const patterns = [
|
|
||||||
{ regex: /^feat(?:ure)?\/(.+)$/i, type: 'feat', scope: null },
|
|
||||||
{ regex: /^fix\/(.+)$/i, type: 'fix', scope: null },
|
|
||||||
{ regex: /^bugfix\/(.+)$/i, type: 'fix', scope: null },
|
|
||||||
{ regex: /^hotfix\/(.+)$/i, type: 'fix', scope: null },
|
|
||||||
{ regex: /^docs?\/(.+)$/i, type: 'docs', scope: null },
|
|
||||||
{ regex: /^refactor\/(.+)$/i, type: 'refactor', scope: null },
|
|
||||||
{ regex: /^test\/(.+)$/i, type: 'test', scope: null },
|
|
||||||
{ regex: /^chore\/(.+)$/i, type: 'chore', scope: null },
|
|
||||||
{ regex: /^style\/(.+)$/i, type: 'style', scope: null },
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const pattern of patterns) {
|
|
||||||
const match = currentBranch.match(pattern.regex);
|
|
||||||
if (match) {
|
|
||||||
// Extract potential scope and description from branch name
|
|
||||||
const branchPart = match[1].replace(/-/g, ' ').replace(/_/g, ' ');
|
|
||||||
return {
|
|
||||||
branch: currentBranch,
|
|
||||||
type: pattern.type,
|
|
||||||
description: branchPart,
|
|
||||||
detected: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for issue references like PROJ-123 or #123
|
|
||||||
const issueMatch = currentBranch.match(/([A-Z]+-\d+|#\d+)/i);
|
|
||||||
if (issueMatch) {
|
|
||||||
return {
|
|
||||||
branch: currentBranch,
|
|
||||||
issueRef: issueMatch[1],
|
|
||||||
detected: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
branch: currentBranch,
|
|
||||||
detected: false,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return { branch: 'unknown', detected: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get commit statistics for the user
|
|
||||||
*/
|
|
||||||
async getCommitStats(days = 30) {
|
|
||||||
try {
|
|
||||||
const since = new Date();
|
|
||||||
since.setDate(since.getDate() - days);
|
|
||||||
|
|
||||||
const log = await this.git.log({ '--since': since.toISOString() });
|
|
||||||
const commits = log.all;
|
|
||||||
|
|
||||||
if (commits.length === 0) {
|
|
||||||
return { totalCommits: 0, hasData: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group by day
|
|
||||||
const byDay = {};
|
|
||||||
commits.forEach(c => {
|
|
||||||
const day = new Date(c.date).toLocaleDateString();
|
|
||||||
byDay[day] = (byDay[day] || 0) + 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate streaks
|
|
||||||
const sortedDays = Object.keys(byDay).sort((a, b) => new Date(b) - new Date(a));
|
|
||||||
let currentStreak = 0;
|
|
||||||
let longestStreak = 0;
|
|
||||||
let tempStreak = 0;
|
|
||||||
|
|
||||||
const today = new Date().toLocaleDateString();
|
|
||||||
const yesterday = new Date(Date.now() - 86400000).toLocaleDateString();
|
|
||||||
|
|
||||||
// Check if today or yesterday has commits
|
|
||||||
if (byDay[today] || byDay[yesterday]) {
|
|
||||||
sortedDays.forEach((day, i) => {
|
|
||||||
if (i === 0) {
|
|
||||||
tempStreak = 1;
|
|
||||||
} else {
|
|
||||||
const prevDate = new Date(sortedDays[i - 1]);
|
|
||||||
const currDate = new Date(day);
|
|
||||||
const diffDays = Math.round((prevDate - currDate) / 86400000);
|
|
||||||
if (diffDays === 1) {
|
|
||||||
tempStreak++;
|
|
||||||
} else {
|
|
||||||
if (tempStreak > longestStreak) longestStreak = tempStreak;
|
|
||||||
tempStreak = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (tempStreak > longestStreak) longestStreak = tempStreak;
|
|
||||||
currentStreak = byDay[today] ? tempStreak : (byDay[yesterday] ? tempStreak : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit types breakdown
|
|
||||||
const typeBreakdown = { feat: 0, fix: 0, docs: 0, style: 0, refactor: 0, test: 0, chore: 0, other: 0 };
|
|
||||||
const conventionalPattern = /^(feat|fix|docs|style|refactor|test|chore|perf|build|ci|revert)/i;
|
|
||||||
|
|
||||||
commits.forEach(c => {
|
|
||||||
const match = c.message.match(conventionalPattern);
|
|
||||||
if (match) {
|
|
||||||
const type = match[1].toLowerCase();
|
|
||||||
if (typeBreakdown.hasOwnProperty(type)) {
|
|
||||||
typeBreakdown[type]++;
|
|
||||||
} else {
|
|
||||||
typeBreakdown.other++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
typeBreakdown.other++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasData: true,
|
|
||||||
totalCommits: commits.length,
|
|
||||||
daysActive: Object.keys(byDay).length,
|
|
||||||
avgPerDay: (commits.length / days).toFixed(1),
|
|
||||||
currentStreak,
|
|
||||||
longestStreak,
|
|
||||||
typeBreakdown,
|
|
||||||
topDay: Object.entries(byDay).sort((a, b) => b[1] - a[1])[0],
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return { hasData: false, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { Intelligence };
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
const { color } = require('./colors');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Progress indicators and user feedback utilities
|
|
||||||
*/
|
|
||||||
class Progress {
|
|
||||||
static spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
||||||
static current = 0;
|
|
||||||
static interval = null;
|
|
||||||
static startTime = null;
|
|
||||||
|
|
||||||
static start(message) {
|
|
||||||
this.startTime = Date.now();
|
|
||||||
this.lastMessage = message;
|
|
||||||
process.stdout.write(`${message} ${this.spinner[0]}`);
|
|
||||||
this.interval = setInterval(() => {
|
|
||||||
this.current = (this.current + 1) % this.spinner.length;
|
|
||||||
process.stdout.write(`\r${message} ${this.spinner[this.current]}`);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
static stop(finalMessage = '') {
|
|
||||||
if (this.interval) {
|
|
||||||
clearInterval(this.interval);
|
|
||||||
this.interval = null;
|
|
||||||
}
|
|
||||||
const elapsed = this.getElapsed();
|
|
||||||
const elapsedStr = elapsed ? ` ${color.dim(`(${elapsed})`)}` : '';
|
|
||||||
// Clear the entire line before writing final message
|
|
||||||
const clearLine = '\r\x1b[K'; // Carriage return + clear to end of line
|
|
||||||
if (finalMessage) {
|
|
||||||
process.stdout.write(`${clearLine}${finalMessage}${elapsedStr}\n`);
|
|
||||||
} else {
|
|
||||||
// Just clear the spinner line completely
|
|
||||||
process.stdout.write(`${clearLine}`);
|
|
||||||
}
|
|
||||||
this.startTime = null;
|
|
||||||
this.lastMessage = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getElapsed() {
|
|
||||||
if (!this.startTime) return null;
|
|
||||||
const ms = Date.now() - this.startTime;
|
|
||||||
if (ms < 1000) return `${ms}ms`;
|
|
||||||
return `${(ms / 1000).toFixed(1)}s`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static success(message) {
|
|
||||||
console.log(`${color.green('✓')} ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
static warning(message) {
|
|
||||||
console.log(`${color.yellow('⚠')} ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
static error(message) {
|
|
||||||
console.log(`${color.red('✗')} ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
static info(message) {
|
|
||||||
console.log(`${color.cyan('ℹ')} ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
static step(step, total, message) {
|
|
||||||
const progress = `[${step}/${total}]`;
|
|
||||||
console.log(`${color.dim(progress)} ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
static cached(message) {
|
|
||||||
console.log(`${color.magenta('⚡')} ${message} ${color.dim('(cached)')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
static tip(message) {
|
|
||||||
console.log(`${color.blue('💡')} ${color.dim('Tip:')} ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
static box(title, lines) {
|
|
||||||
const maxLen = Math.max(title.length, ...lines.map(l => l.replace(/\x1b\[[0-9;]*m/g, '').length));
|
|
||||||
const top = `┌${'─'.repeat(maxLen + 2)}┐`;
|
|
||||||
const bottom = `└${'─'.repeat(maxLen + 2)}┘`;
|
|
||||||
const titleLine = `│ ${color.bold(title.padEnd(maxLen))} │`;
|
|
||||||
|
|
||||||
console.log(top);
|
|
||||||
console.log(titleLine);
|
|
||||||
console.log(`├${'─'.repeat(maxLen + 2)}┤`);
|
|
||||||
lines.forEach(line => {
|
|
||||||
const plainLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
||||||
const padding = ' '.repeat(maxLen - plainLen);
|
|
||||||
console.log(`│ ${line}${padding} │`);
|
|
||||||
});
|
|
||||||
console.log(bottom);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Random tips for contextual help
|
|
||||||
static tips = [
|
|
||||||
'Use `g int` for interactive commit wizard with multiple AI suggestions',
|
|
||||||
'Use `g o` to commit and push in one command',
|
|
||||||
'Use `g sg --multiple` to get 3 different commit message suggestions',
|
|
||||||
'Configure your preferences with `g config --set key=value`',
|
|
||||||
'Use `--all` flag to auto-stage all changes',
|
|
||||||
'Use `g a` to amend your last commit',
|
|
||||||
'Use `g sync --rebase` for cleaner history',
|
|
||||||
'Set GEMINI_API_KEY for free AI-powered commits',
|
|
||||||
'Use `g wip` for quick work-in-progress commits',
|
|
||||||
'Use `g stats` to see your commit statistics',
|
|
||||||
];
|
|
||||||
|
|
||||||
static showRandomTip() {
|
|
||||||
const shouldShow = Math.random() < 0.15; // 15% chance
|
|
||||||
if (shouldShow) {
|
|
||||||
const tip = this.tips[Math.floor(Math.random() * this.tips.length)];
|
|
||||||
console.log();
|
|
||||||
this.tip(tip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { Progress };
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
const simpleGit = require('simple-git');
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
class S4Versioning {
|
|
||||||
constructor(git) {
|
|
||||||
this.git = git || simpleGit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regex for parsing S4 version
|
|
||||||
static get REGEX() {
|
|
||||||
return /^([0-9]+)\.([0-9]+)\.([0-9]+)-([a-z]+)\.([0-9]+)\+([0-9]{8})\.([0-9]{4})\.([0-9a-f]+)\.([a-zA-Z0-9._\/-]+)$/;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse an S4 version string into components
|
|
||||||
*/
|
|
||||||
static parse(versionStr) {
|
|
||||||
if (!versionStr) return null;
|
|
||||||
const match = versionStr.match(S4Versioning.REGEX);
|
|
||||||
if (!match) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
major: parseInt(match[1]),
|
|
||||||
minor: parseInt(match[2]),
|
|
||||||
patch: parseInt(match[3]),
|
|
||||||
stage: match[4],
|
|
||||||
build: parseInt(match[5]),
|
|
||||||
date: match[6],
|
|
||||||
time: match[7],
|
|
||||||
commit: match[8],
|
|
||||||
branch: match[9],
|
|
||||||
original: versionStr
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate S4 version string from components
|
|
||||||
*/
|
|
||||||
static stringify(components) {
|
|
||||||
const { major, minor, patch, stage, build, date, time, commit, branch } = components;
|
|
||||||
return `${major}.${minor}.${patch}-${stage}.${build}+${date}.${time}.${commit}.${branch}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the latest S4 version from Git Tags
|
|
||||||
*/
|
|
||||||
async getCurrentVersion() {
|
|
||||||
try {
|
|
||||||
// Get all tags
|
|
||||||
const tags = await this.git.tags();
|
|
||||||
if (!tags.all || tags.all.length === 0) {
|
|
||||||
return null; // No tags yet
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter and sort S4 tags
|
|
||||||
// Sorting strategy: We rely on the date+time in the S4 string for chronological order
|
|
||||||
// but S4 strings format puts SemVer first.
|
|
||||||
// Let's parse all valid S4 tags and sort them.
|
|
||||||
|
|
||||||
const parsedTags = tags.all
|
|
||||||
.map(t => ({ tag: t, parsed: S4Versioning.parse(t) }))
|
|
||||||
.filter(item => item.parsed !== null);
|
|
||||||
|
|
||||||
if (parsedTags.length === 0) return null;
|
|
||||||
|
|
||||||
// Sort logic:
|
|
||||||
// 1. Major.Minor.Patch
|
|
||||||
// 2. Date.Time
|
|
||||||
// 3. Build
|
|
||||||
|
|
||||||
parsedTags.sort((a, b) => {
|
|
||||||
const pA = a.parsed;
|
|
||||||
const pB = b.parsed;
|
|
||||||
|
|
||||||
if (pA.major !== pB.major) return pA.major - pB.major;
|
|
||||||
if (pA.minor !== pB.minor) return pA.minor - pB.minor;
|
|
||||||
if (pA.patch !== pB.patch) return pA.patch - pB.patch;
|
|
||||||
|
|
||||||
// Compare Date (YYYYMMDD)
|
|
||||||
const dateA = parseInt(pA.date);
|
|
||||||
const dateB = parseInt(pB.date);
|
|
||||||
if (dateA !== dateB) return dateA - dateB;
|
|
||||||
|
|
||||||
// Compare Time (HHMM)
|
|
||||||
const timeA = parseInt(pA.time);
|
|
||||||
const timeB = parseInt(pB.time);
|
|
||||||
if (timeA !== timeB) return timeA - timeB;
|
|
||||||
|
|
||||||
return pA.build - pB.build;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return the latest
|
|
||||||
return parsedTags[parsedTags.length - 1].tag;
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrate a standard SemVer or empty version to S4
|
|
||||||
*/
|
|
||||||
async migrate(currentVersion) {
|
|
||||||
// Basic SemVer parsing (loose)
|
|
||||||
const semVerMatch = currentVersion.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([a-zA-Z0-9.]+))?/);
|
|
||||||
|
|
||||||
let base = {
|
|
||||||
major: 0, minor: 0, patch: 0, stage: 'dev', build: 1
|
|
||||||
};
|
|
||||||
|
|
||||||
if (semVerMatch) {
|
|
||||||
base.major = parseInt(semVerMatch[1]);
|
|
||||||
base.minor = parseInt(semVerMatch[2]);
|
|
||||||
base.patch = parseInt(semVerMatch[3]);
|
|
||||||
// Attempt to salvage stage if present, else default to dev
|
|
||||||
if (semVerMatch[4]) {
|
|
||||||
const parts = semVerMatch[4].split('.');
|
|
||||||
const possibleStage = parts.find(p => /^[a-z]+$/.test(p));
|
|
||||||
if (possibleStage) base.stage = possibleStage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ctx = await this.getContext();
|
|
||||||
return S4Versioning.stringify({ ...base, ...ctx });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current Git and Time context
|
|
||||||
*/
|
|
||||||
async getContext() {
|
|
||||||
const now = new Date();
|
|
||||||
const pad = (n) => n.toString().padStart(2, '0');
|
|
||||||
const date = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
|
|
||||||
const time = `${pad(now.getHours())}${pad(now.getMinutes())}`;
|
|
||||||
|
|
||||||
let commit = '0000000';
|
|
||||||
let branch = 'unknown';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const isRepo = await this.git.checkIsRepo();
|
|
||||||
if (isRepo) {
|
|
||||||
commit = (await this.git.revparse(['--short=7', 'HEAD'])).trim();
|
|
||||||
branch = (await this.git.revparse(['--abbrev-ref', 'HEAD'])).trim();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore git errors, stick to defaults
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanitize branch name for S4 compliance
|
|
||||||
branch = branch.replace(/[^a-zA-Z0-9._\/-]/g, '-');
|
|
||||||
|
|
||||||
return { date, time, commit, branch };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate next version based on bump type
|
|
||||||
*/
|
|
||||||
async bump(currentStr, type = 'auto', stageVal = null) {
|
|
||||||
let parsed = S4Versioning.parse(currentStr);
|
|
||||||
|
|
||||||
// If not S4, migrate first
|
|
||||||
if (!parsed) {
|
|
||||||
const migrated = await this.migrate(currentStr);
|
|
||||||
parsed = S4Versioning.parse(migrated);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ctx = await this.getContext();
|
|
||||||
const next = { ...parsed, ...ctx }; // Update context (date, time, commit, branch)
|
|
||||||
|
|
||||||
// Handle stage change
|
|
||||||
if (stageVal && stageVal !== parsed.stage) {
|
|
||||||
next.stage = stageVal;
|
|
||||||
next.build = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logic for bumping
|
|
||||||
switch (type) {
|
|
||||||
case 'major':
|
|
||||||
next.major++;
|
|
||||||
next.minor = 0;
|
|
||||||
next.patch = 0;
|
|
||||||
next.build = 1;
|
|
||||||
break;
|
|
||||||
case 'minor':
|
|
||||||
next.minor++;
|
|
||||||
next.patch = 0;
|
|
||||||
next.build = 1;
|
|
||||||
break;
|
|
||||||
case 'patch':
|
|
||||||
next.patch++;
|
|
||||||
next.build = 1;
|
|
||||||
break;
|
|
||||||
case 'auto':
|
|
||||||
// Smart Auto Logic
|
|
||||||
|
|
||||||
// 1. If Date Changed, reset build to 1
|
|
||||||
if (parsed.date !== ctx.date) {
|
|
||||||
next.build = 1;
|
|
||||||
}
|
|
||||||
// 2. If Date is SAME
|
|
||||||
else {
|
|
||||||
// If Commit Changed -> new build
|
|
||||||
if (parsed.commit !== ctx.commit) {
|
|
||||||
next.build = parsed.build + 1;
|
|
||||||
}
|
|
||||||
// If same commit & same date, usually we don't need a new tag
|
|
||||||
// unless user forces it.
|
|
||||||
// For safety, increment build to ensure uniqueness if they insisted on running `g v`.
|
|
||||||
else {
|
|
||||||
next.build = parsed.build + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return S4Versioning.stringify(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { S4Versioning };
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "get_status",
|
|
||||||
"description": "Get enhanced git status with AI insights, file modification stats, and branch context. Use this to understand the current state of the repository.",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "analyze_history",
|
|
||||||
"description": "Analyze recent commit history, including authors, conventions, and activity patterns.",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"limit": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Number of commits to analyze (default: 10)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "version_info",
|
|
||||||
"description": "Get current project version information using S4 versioning (Semantic + Date/Time).",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "generate_commit_message",
|
|
||||||
"description": "Generate a high-quality commit message based on currently staged changes.",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"context": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Optional context or user instruction for the message generation"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "run_git_command",
|
|
||||||
"description": "Execute a raw git command safely. Use this for standard git operations not covered by other tools.",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"command": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The full git command to run (e.g., 'log -n 5', 'branch --list')"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"command"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GIMS MCP Server
|
|
||||||
*
|
|
||||||
* Exposes GIMS capabilities as Model Context Protocol (MCP) tools.
|
|
||||||
* Usage: node mcp/index.js
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
|
|
||||||
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
||||||
const {
|
|
||||||
CallToolRequestSchema,
|
|
||||||
ListToolsRequestSchema,
|
|
||||||
} = require("@modelcontextprotocol/sdk/types.js");
|
|
||||||
const { tools } = require('../bin/lib/ai/interface');
|
|
||||||
|
|
||||||
// Create the server
|
|
||||||
const server = new Server(
|
|
||||||
{
|
|
||||||
name: "gims-mcp-server",
|
|
||||||
version: require('../package.json').version,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
capabilities: {
|
|
||||||
tools: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for listing available tools
|
|
||||||
*/
|
|
||||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
||||||
return {
|
|
||||||
tools: tools.map((tool) => ({
|
|
||||||
name: tool.name,
|
|
||||||
description: tool.description,
|
|
||||||
inputSchema: tool.inputSchema,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for calling tools
|
|
||||||
*/
|
|
||||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
||||||
const toolName = request.params.name;
|
|
||||||
const toolArgs = request.params.arguments;
|
|
||||||
|
|
||||||
const tool = tools.find((t) => t.name === toolName);
|
|
||||||
|
|
||||||
if (!tool) {
|
|
||||||
throw new Error(`Unknown tool: ${toolName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await tool.handler(toolArgs);
|
|
||||||
|
|
||||||
// MCP expects content to be an array of text/image objects
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: JSON.stringify(result, null, 2),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: `Error executing ${toolName}: ${error.message}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isError: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the server
|
|
||||||
*/
|
|
||||||
async function main() {
|
|
||||||
const transport = new StdioServerTransport();
|
|
||||||
await server.connect(transport);
|
|
||||||
console.error("GIMS MCP Server running on stdio");
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error("Server error:", error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
Generated
-1960
File diff suppressed because it is too large
Load Diff
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "gims",
|
|
||||||
"version": "0.9.1",
|
|
||||||
"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'\""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
const { OpenAI } = require('openai');
|
|
||||||
const process = require('process');
|
|
||||||
|
|
||||||
async function testOpenAI() {
|
|
||||||
console.log('Testing OpenAI connection...');
|
|
||||||
console.log('API Key present:', !!process.env.OPENAI_API_KEY);
|
|
||||||
console.log('Model:', 'gpt-5.2-2025-12-11');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
||||||
|
|
||||||
// Log the base URL to see if it's custom
|
|
||||||
console.log('Base URL:', openai.baseURL);
|
|
||||||
|
|
||||||
const response = await openai.chat.completions.create({
|
|
||||||
model: 'gpt-5.2-2025-12-11',
|
|
||||||
messages: [{ role: 'user', content: 'Say hello' }],
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Success!');
|
|
||||||
console.log('Content:', response.choices[0].message.content);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('ERROR OCCURRED:');
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testOpenAI();
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const { tools } = require('../bin/lib/ai/interface');
|
|
||||||
|
|
||||||
const schema = tools.map(tool => ({
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: tool.name,
|
|
||||||
description: tool.description,
|
|
||||||
parameters: tool.inputSchema
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
const outputPath = path.resolve(__dirname, '../bin/tools-schema.json');
|
|
||||||
fs.writeFileSync(outputPath, JSON.stringify(schema, null, 2));
|
|
||||||
|
|
||||||
console.log(`✅ Generated OpenAI-compatible tool schemas at: ${outputPath}`);
|
|
||||||
console.log(`Total tools: ${tools.length}`);
|
|
||||||
Reference in New Issue
Block a user