ai-ship-cli 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +3 -0
- package/.prettierrc +7 -0
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/ai/gemini.js +34 -0
- package/dist/ai/ollama.js +36 -0
- package/dist/analyzers/analyzer.js +93 -0
- package/dist/analyzers/compressBranchSummary.js +12 -0
- package/dist/analyzers/configAnalyzer.js +18 -0
- package/dist/analyzers/detectSignals.js +18 -0
- package/dist/analyzers/markupAnalyzer.js +26 -0
- package/dist/commands/commit/customAdd.js +1 -0
- package/dist/commands/commit/startCommit.js +155 -0
- package/dist/commands/commit.js +21 -0
- package/dist/commands/config/deleteKey.js +15 -0
- package/dist/commands/config.js +65 -0
- package/dist/commands/git/startCheckout.js +62 -0
- package/dist/commands/git/startCommit.js +91 -0
- package/dist/commands/git/startPR.js +51 -0
- package/dist/commands/git/startPush.js +24 -0
- package/dist/commands/git/startWorkflow.js +71 -0
- package/dist/commands/github/github.js +63 -0
- package/dist/commands/pr.js +1 -0
- package/dist/index.js +38 -0
- package/dist/utils/ai.js +22 -0
- package/dist/utils/asyncExecuter.js +35 -0
- package/dist/utils/files.js +28 -0
- package/dist/utils/git.js +106 -0
- package/dist/utils/github.js +13 -0
- package/dist/utils/helper.js +130 -0
- package/dist/utils/inputs.js +20 -0
- package/dist/utils/inquirer.js +79 -0
- package/dist/utils/parser.js +54 -0
- package/dist/utils/print.js +25 -0
- package/dist/utils/prompts.js +206 -0
- package/dist/utils/runCommit.js +17 -0
- package/dist/utils/runConfig.js +35 -0
- package/docs/commands.md +106 -0
- package/package.json +44 -0
- package/src/ai/gemini.ts +27 -0
- package/src/ai/ollama.ts +38 -0
- package/src/analyzers/analyzer.ts +117 -0
- package/src/analyzers/compressBranchSummary.ts +16 -0
- package/src/analyzers/configAnalyzer.ts +17 -0
- package/src/analyzers/detectSignals.ts +13 -0
- package/src/analyzers/markupAnalyzer.ts +25 -0
- package/src/commands/commit.ts +18 -0
- package/src/commands/config.ts +73 -0
- package/src/commands/git/startCheckout.ts +97 -0
- package/src/commands/git/startCommit.ts +108 -0
- package/src/commands/git/startPR.ts +66 -0
- package/src/commands/git/startPush.ts +18 -0
- package/src/commands/git/startWorkflow.ts +71 -0
- package/src/commands/github/github.ts +72 -0
- package/src/commands/pr.ts +0 -0
- package/src/index.ts +40 -0
- package/src/utils/ai.ts +30 -0
- package/src/utils/asyncExecuter.ts +39 -0
- package/src/utils/files.ts +30 -0
- package/src/utils/git.ts +108 -0
- package/src/utils/github.ts +19 -0
- package/src/utils/helper.ts +145 -0
- package/src/utils/inputs.ts +15 -0
- package/src/utils/inquirer.ts +99 -0
- package/src/utils/parser.ts +58 -0
- package/src/utils/print.ts +16 -0
- package/src/utils/prompts.ts +234 -0
- package/tsconfig.json +11 -0
package/.prettierignore
ADDED
package/.prettierrc
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Diganta Kr Banik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img alt="AI-Ship Logo" src="https://img.icons8.com/nolan/256/spaceship.png" width="120" />
|
|
3
|
+
<h1>🚀 AI-SHIP</h1>
|
|
4
|
+
<p><strong>Your ultimate AI-powered terminal assistant for seamless Git workflows.</strong></p>
|
|
5
|
+
<p>Dynamically analyzes your code diffs to automatically write robust and contextual commit messages, generate meaningful branch names, push your changes, and spin up intelligent GitHub Pull Requests—all without ever leaving your terminal.</p>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<br />
|
|
9
|
+
|
|
10
|
+
## ✨ Features
|
|
11
|
+
|
|
12
|
+
- **🧠 Deep Code Analysis:** AI-Ship doesn't just read filenames. It reads your raw `git diff` outputs, intelligently filters out noise (like lockfiles), and evaluates the actual intent of your code changes.
|
|
13
|
+
- **📝 Automated Intelligent Commits:** Say goodbye to "fixed bug" or "updated code". AI-Ship drafts rich, standard-compliant commit messages highlighting semantic changes.
|
|
14
|
+
- **🌿 Smart Branch Name Generation:** Need a new feature branch? Throw in `--new-branch` to let AI-Ship read your commit's context, draft a descriptive semantic branch name (e.g. `feature/implement-user-auth`), automatically apply branch collision defenses, and check it out for you.
|
|
15
|
+
- **🚀 One-command PR Pipelines:** Complete your entire development phase in one go. Using `ai-ship commit --push --pr` takes your staged changes, commits them intelligibly, pushes them directly to `origin`, and opens a full contextual Pull Request over the GitHub CLI.
|
|
16
|
+
- **🤖 Provider Agnostic:** Works beautifully with **Google Gemini** (Cloud) out-of-the-box or **Ollama** for entirely private, local execution.
|
|
17
|
+
- **🛡️ Dry Runs & Pre-flights:** Want to see what the AI cooks up without mutating your local Git repository? Use `--dry-run`.
|
|
18
|
+
- **✍️ Interactive Refinements:** Don't like the drafted message? Instantly edit it, request a retry, or skip.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 📦 Prerequisites
|
|
23
|
+
|
|
24
|
+
Before deploying AI-Ship, ensure your machine meets the following structural requirements:
|
|
25
|
+
1. **[Node.js](https://nodejs.org/)** (v18 or higher recommended)
|
|
26
|
+
2. **[Git](https://git-scm.com/)** installed and initialized in your working repository.
|
|
27
|
+
3. **[GitHub CLI (`gh`)](https://cli.github.com/)** installed and authenticated (required exclusively if you use `--pr` flows).
|
|
28
|
+
4. **An API Access key** for Google Gemini, **OR** a local instance of [Ollama](https://ollama.com/) running locally.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 🛠️ Installation & Setup
|
|
33
|
+
|
|
34
|
+
For now, AI-Ship can be run directly from source.
|
|
35
|
+
|
|
36
|
+
1. **Clone the repository:**
|
|
37
|
+
```bash
|
|
38
|
+
git clone https://github.com/developer-diganta/ai-ship.git
|
|
39
|
+
cd ai-ship
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
2. **Install dependencies:**
|
|
43
|
+
```bash
|
|
44
|
+
npm install
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
3. **Build the CLI executable:**
|
|
48
|
+
```bash
|
|
49
|
+
npm run build
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
4. **Link globally (Optional but recommended):**
|
|
53
|
+
```bash
|
|
54
|
+
npm link
|
|
55
|
+
```
|
|
56
|
+
*This globally registers the `ai-ship` command in your terminal so it can be initiated in any Git repository on your system.*
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## ⚙️ Configuration
|
|
61
|
+
|
|
62
|
+
Control how AI-Ship operates directly from the CLI via the `config` command.
|
|
63
|
+
|
|
64
|
+
### 🔑 Set up your API Key (Cloud/Gemini)
|
|
65
|
+
To utilize the Gemini API connection:
|
|
66
|
+
```bash
|
|
67
|
+
ai-ship config --add-key
|
|
68
|
+
```
|
|
69
|
+
*It will securely prompt for your API key and store it locally.*
|
|
70
|
+
|
|
71
|
+
To remove your key:
|
|
72
|
+
```bash
|
|
73
|
+
ai-ship config --delete-key
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 🧠 Change AI Provider (Local vs Cloud)
|
|
77
|
+
If you prefer running a local execution without making network API calls to Cloud AI models, you can switch providers. *(Requires a local Ollama server)*:
|
|
78
|
+
```bash
|
|
79
|
+
ai-ship config set provider local
|
|
80
|
+
```
|
|
81
|
+
To switch back to the cloud via Google Gemini:
|
|
82
|
+
```bash
|
|
83
|
+
ai-ship config set provider cloud
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 🧾 View Current Config
|
|
87
|
+
```bash
|
|
88
|
+
ai-ship config show --verbose
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 🚢 Usage
|
|
94
|
+
|
|
95
|
+
At its core, `ai-ship` hooks into your uncommitted, staged files.
|
|
96
|
+
|
|
97
|
+
### Basic Usage
|
|
98
|
+
|
|
99
|
+
Stage your files as usual (`git add .`), then draft an AI commit:
|
|
100
|
+
```bash
|
|
101
|
+
ai-ship commit
|
|
102
|
+
```
|
|
103
|
+
> *Note: If you run `ai-ship commit` without staging, AI-Ship will automatically run `git add -A` for you!*
|
|
104
|
+
|
|
105
|
+
You can also pass specific files to be explicitly staged:
|
|
106
|
+
```bash
|
|
107
|
+
ai-ship commit src/index.ts src/utils/helper.ts
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Advanced Pipeline Workflows
|
|
111
|
+
|
|
112
|
+
AI-Ship can condense the 5 basic Git commands down into **one single instruction**.
|
|
113
|
+
|
|
114
|
+
**🔥 The Ultimate CLI Combo (Commit, Branch, Push, PR):**
|
|
115
|
+
```bash
|
|
116
|
+
ai-ship commit --new-branch --push --pr --yes
|
|
117
|
+
```
|
|
118
|
+
*What this single command achieved:*
|
|
119
|
+
1. Auto-staged all tracked changes.
|
|
120
|
+
2. Read the unified diff and generated a beautiful Commit Message.
|
|
121
|
+
3. Examined that context and automatically made a new branch (e.g., `feature/add-payment-gate`).
|
|
122
|
+
4. Directly pushed the new upstream branch to origin.
|
|
123
|
+
5. Invoked the `gh` CLI to summarize the commit history into a Pull Request Description.
|
|
124
|
+
6. Auto-published the PR on GitHub without asking for manual confirmation (`--yes`).
|
|
125
|
+
|
|
126
|
+
### 🎌 Command Flags
|
|
127
|
+
Append these arguments upon the `commit` hook to modify AI-Ship's behavior:
|
|
128
|
+
|
|
129
|
+
| Flag | Action |
|
|
130
|
+
| --- | --- |
|
|
131
|
+
| `--new-branch` | Analyzes commit context, drafts a semantic branch name, & securely checks it out. |
|
|
132
|
+
| `--push` | Triggers a `git push`. If there isn't an upstream anchor configured, securely configures tracking mappings for you. |
|
|
133
|
+
| `--pr` | Utilizes the GitHub CLI to publish an intelligent PR onto default target branches. Prompts interactions manually if omitted. |
|
|
134
|
+
| `--target-branch <branch>` | Manually overrides the base branch point targeting your `--pr`. |
|
|
135
|
+
| `--yes` | Headless execution. Skips interactive "Edit/Continue/Cancel" refinements and accepts the AI's first guess automatically. |
|
|
136
|
+
| `--dry-run` | Reads files, interfaces with the AI layer, and outputs responses without actually performing Git mutations. Great for observing syntax. |
|
|
137
|
+
| `--model <provider>` | Inline injection overriding the globally chosen logic model (e.g `--model local` or `--model cloud`). |
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 🤝 Contributing
|
|
142
|
+
|
|
143
|
+
Contributions are heavily welcomed!
|
|
144
|
+
1. **Fork** the repository
|
|
145
|
+
2. Implement your magic fix or feature branch (`git checkout -b feature/magic-fix`)
|
|
146
|
+
3. Pass through Prettier & Typescript integrations by running `npm run build && npm run format`
|
|
147
|
+
4. Use AI-Ship to push your PR logic gracefully.
|
|
148
|
+
5. Submit your **Pull Request**.
|
|
149
|
+
|
|
150
|
+
If you locate any discrepancies or issues, please [file a new GitHub Issue](https://github.com/developer-diganta/ai-ship/issues).
|
|
151
|
+
|
|
152
|
+
<br />
|
|
153
|
+
|
|
154
|
+
<div align="center">
|
|
155
|
+
<p>Built with 🩵 by <a href="https://github.com/developer-diganta">Diganta</a></p>
|
|
156
|
+
</div>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateWithGemini = void 0;
|
|
7
|
+
const genai_1 = require("@google/genai");
|
|
8
|
+
const helper_1 = require("../utils/helper");
|
|
9
|
+
const inputs_1 = require("../utils/inputs");
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
|
|
12
|
+
let apiKey = (0, helper_1.getCurrentConfig)('geminiApiKey') || GEMINI_API_KEY;
|
|
13
|
+
let ai = new genai_1.GoogleGenAI({ apiKey });
|
|
14
|
+
const generateWithGemini = async (prompt) => {
|
|
15
|
+
try {
|
|
16
|
+
if (!apiKey) {
|
|
17
|
+
console.log('Gemini API key not found.');
|
|
18
|
+
apiKey = await (0, inputs_1.askApiKey)();
|
|
19
|
+
(0, helper_1.saveValueToConfig)('geminiApiKey', apiKey);
|
|
20
|
+
ai = new genai_1.GoogleGenAI({ apiKey });
|
|
21
|
+
console.log('API key saved!');
|
|
22
|
+
}
|
|
23
|
+
const response = await ai.models.generateContent({
|
|
24
|
+
model: 'gemini-2.5-flash',
|
|
25
|
+
contents: prompt,
|
|
26
|
+
});
|
|
27
|
+
return response.text || '';
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
console.log(chalk_1.default.red(`We ran into an error: ${chalk_1.default.bold.red(err)}`));
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
exports.generateWithGemini = generateWithGemini;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateWithGemma = void 0;
|
|
4
|
+
const helper_1 = require("../utils/helper");
|
|
5
|
+
const generateWithGemma = async (prompt) => {
|
|
6
|
+
try {
|
|
7
|
+
const endpoint = (0, helper_1.getCurrentConfig)('localEndpoint') || 'http://127.0.0.1:11434';
|
|
8
|
+
const model = (0, helper_1.getCurrentConfig)('model') || 'gemma:2b';
|
|
9
|
+
const res = await fetch(`${endpoint}/api/generate`, {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
headers: {
|
|
12
|
+
'Content-Type': 'application/json',
|
|
13
|
+
},
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
model: model,
|
|
16
|
+
prompt,
|
|
17
|
+
stream: false,
|
|
18
|
+
options: {
|
|
19
|
+
temperature: 0.1,
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
const data = await res.json();
|
|
24
|
+
if (!res.ok || data.error) {
|
|
25
|
+
throw new Error(data.error || `HTTP error! status: ${res.status}`);
|
|
26
|
+
}
|
|
27
|
+
if (data.response) {
|
|
28
|
+
return data.response.trim();
|
|
29
|
+
}
|
|
30
|
+
throw new Error('Unexpected response format from local model');
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
throw new Error(`Failed to connect to local model: ${e.message}`);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
exports.generateWithGemma = generateWithGemma;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.analyzeDiff = void 0;
|
|
7
|
+
const detectSignals_1 = require("./detectSignals");
|
|
8
|
+
const markupAnalyzer_1 = __importDefault(require("./markupAnalyzer"));
|
|
9
|
+
const configAnalyzer_1 = __importDefault(require("./configAnalyzer"));
|
|
10
|
+
const CODE_EXT = [
|
|
11
|
+
'.js',
|
|
12
|
+
'.ts',
|
|
13
|
+
'.jsx',
|
|
14
|
+
'.tsx',
|
|
15
|
+
'.py',
|
|
16
|
+
'.java',
|
|
17
|
+
'.go',
|
|
18
|
+
'.rs',
|
|
19
|
+
'.cpp',
|
|
20
|
+
'.c',
|
|
21
|
+
'.h',
|
|
22
|
+
'.hpp',
|
|
23
|
+
'.cs',
|
|
24
|
+
'.php',
|
|
25
|
+
'.rb',
|
|
26
|
+
];
|
|
27
|
+
const MARKUP_EXT = ['.html', '.css', '.scss', '.less'];
|
|
28
|
+
const CONFIG_EXT = ['.json', '.yaml', '.yml', '.toml'];
|
|
29
|
+
const getFileType = (file) => {
|
|
30
|
+
const lower = file.toLowerCase();
|
|
31
|
+
if (CODE_EXT.some((ext) => lower.endsWith(ext)))
|
|
32
|
+
return 'code';
|
|
33
|
+
if (MARKUP_EXT.some((ext) => lower.endsWith(ext)))
|
|
34
|
+
return 'markup';
|
|
35
|
+
if (CONFIG_EXT.some((ext) => lower.endsWith(ext)) ||
|
|
36
|
+
lower === 'dockerfile' ||
|
|
37
|
+
lower.includes('package.json'))
|
|
38
|
+
return 'config';
|
|
39
|
+
return 'other';
|
|
40
|
+
};
|
|
41
|
+
const analyzeDiff = (diff) => {
|
|
42
|
+
try {
|
|
43
|
+
const files = diff.split('diff --git').filter(Boolean);
|
|
44
|
+
const summaries = [];
|
|
45
|
+
for (const chunk of files) {
|
|
46
|
+
const lines = chunk.split('\n');
|
|
47
|
+
const fileMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/);
|
|
48
|
+
const file = fileMatch?.[2] || 'unknown';
|
|
49
|
+
const fileType = getFileType(file);
|
|
50
|
+
if (fileType === 'markup') {
|
|
51
|
+
summaries.push((0, markupAnalyzer_1.default)(file, lines));
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (fileType === 'config') {
|
|
55
|
+
summaries.push((0, configAnalyzer_1.default)(file, lines));
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
// default = code analyzer
|
|
59
|
+
let additions = 0;
|
|
60
|
+
let deletions = 0;
|
|
61
|
+
const snippet = [];
|
|
62
|
+
const signals = [];
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
65
|
+
additions++;
|
|
66
|
+
const code = line.slice(1).trim();
|
|
67
|
+
if (snippet.length < 20)
|
|
68
|
+
snippet.push(`+ ${code}`);
|
|
69
|
+
(0, detectSignals_1.detectSignals)(code, signals);
|
|
70
|
+
}
|
|
71
|
+
if (line.startsWith('-') && !line.startsWith('---')) {
|
|
72
|
+
deletions++;
|
|
73
|
+
const code = line.slice(1).trim();
|
|
74
|
+
if (snippet.length < 20)
|
|
75
|
+
snippet.push(`- ${code}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
summaries.push({
|
|
79
|
+
file,
|
|
80
|
+
additions,
|
|
81
|
+
deletions,
|
|
82
|
+
signals: [...new Set(signals)],
|
|
83
|
+
snippet,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return summaries;
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
console.log({ e });
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
exports.analyzeDiff = analyzeDiff;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.compressBranchSummary = void 0;
|
|
4
|
+
const compressBranchSummary = (summary) => {
|
|
5
|
+
return summary
|
|
6
|
+
.slice(0, 8) // limit files
|
|
7
|
+
.map((s) => ({
|
|
8
|
+
file: s.file,
|
|
9
|
+
signals: s.signals,
|
|
10
|
+
}));
|
|
11
|
+
};
|
|
12
|
+
exports.compressBranchSummary = compressBranchSummary;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = (file, lines) => {
|
|
4
|
+
const snippet = [];
|
|
5
|
+
for (const line of lines) {
|
|
6
|
+
if (line.startsWith('+') || line.startsWith('-')) {
|
|
7
|
+
if (snippet.length < 30)
|
|
8
|
+
snippet.push(line);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
file,
|
|
13
|
+
additions: snippet.filter((l) => l.startsWith('+')).length,
|
|
14
|
+
deletions: snippet.filter((l) => l.startsWith('-')).length,
|
|
15
|
+
signals: ['config updated'],
|
|
16
|
+
snippet,
|
|
17
|
+
};
|
|
18
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectSignals = void 0;
|
|
4
|
+
const detectSignals = (line, signals) => {
|
|
5
|
+
if (/function\s+\w+/.test(line))
|
|
6
|
+
signals.push('function added');
|
|
7
|
+
if (/class\s+\w+/.test(line))
|
|
8
|
+
signals.push('class added');
|
|
9
|
+
if (/import\s+/.test(line))
|
|
10
|
+
signals.push('imports updated');
|
|
11
|
+
if (/export\s+/.test(line))
|
|
12
|
+
signals.push('exports updated');
|
|
13
|
+
if (/FROM\s+/.test(line))
|
|
14
|
+
signals.push('docker base image change');
|
|
15
|
+
if (/version/.test(line))
|
|
16
|
+
signals.push('version updated');
|
|
17
|
+
};
|
|
18
|
+
exports.detectSignals = detectSignals;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = (file, lines) => {
|
|
4
|
+
const snippet = [];
|
|
5
|
+
let additions = 0;
|
|
6
|
+
let deletions = 0;
|
|
7
|
+
for (const line of lines) {
|
|
8
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
9
|
+
additions++;
|
|
10
|
+
if (snippet.length < 20)
|
|
11
|
+
snippet.push(line);
|
|
12
|
+
}
|
|
13
|
+
if (line.startsWith('-') && !line.startsWith('---')) {
|
|
14
|
+
deletions++;
|
|
15
|
+
if (snippet.length < 20)
|
|
16
|
+
snippet.push(line);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
file,
|
|
21
|
+
additions,
|
|
22
|
+
deletions,
|
|
23
|
+
signals: ['markup updated'],
|
|
24
|
+
snippet,
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
7
|
+
const helper_1 = require("../../utils/helper");
|
|
8
|
+
const inputs_1 = require("../../utils/inputs");
|
|
9
|
+
const git_1 = require("../../utils/git");
|
|
10
|
+
const files_1 = require("../../utils/files");
|
|
11
|
+
const prompts_1 = require("../../utils/prompts");
|
|
12
|
+
const parser_1 = require("../../utils/parser");
|
|
13
|
+
const genai_1 = require("@google/genai");
|
|
14
|
+
const analyzer_1 = require("../../analyzers/analyzer");
|
|
15
|
+
const compressBranchSummary_1 = require("../../analyzers/compressBranchSummary");
|
|
16
|
+
const ollama_1 = require("../../ai/ollama");
|
|
17
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
18
|
+
// @ts-ignore
|
|
19
|
+
const enquirer_1 = require("enquirer");
|
|
20
|
+
const ora_1 = __importDefault(require("ora"));
|
|
21
|
+
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
|
|
22
|
+
let apiKey = (0, helper_1.loadApiKey)();
|
|
23
|
+
exports.default = async () => {
|
|
24
|
+
try {
|
|
25
|
+
if (!apiKey) {
|
|
26
|
+
console.log('Gemini API key not found.');
|
|
27
|
+
apiKey = await (0, inputs_1.askApiKey)();
|
|
28
|
+
(0, helper_1.saveApiKey)(apiKey);
|
|
29
|
+
console.log('API key saved!');
|
|
30
|
+
}
|
|
31
|
+
let ai = new genai_1.GoogleGenAI({ apiKey });
|
|
32
|
+
console.log('');
|
|
33
|
+
console.log(chalk_1.default.bold.bgBlue(' 🚀 AI-SHIP ') + chalk_1.default.bold.blue(' Commit Generator '));
|
|
34
|
+
console.log(chalk_1.default.dim('==================================='));
|
|
35
|
+
console.log('');
|
|
36
|
+
// 1️⃣ Get staged files
|
|
37
|
+
const scanSpinner = (0, ora_1.default)('Scanning staged files...').start();
|
|
38
|
+
const filesChanged = await (0, git_1.getFilesChanged)();
|
|
39
|
+
if (!filesChanged.length) {
|
|
40
|
+
scanSpinner.fail(chalk_1.default.yellow('No staged files detected.'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// 2️⃣ Expand directories
|
|
44
|
+
const expandedFiles = (0, files_1.expandDirectories)(filesChanged);
|
|
45
|
+
// 3️⃣ Extract filenames
|
|
46
|
+
let filenames = expandedFiles.map((f) => f.file);
|
|
47
|
+
// 4️⃣ Filter noise files
|
|
48
|
+
filenames = (0, parser_1.filterNoiseFiles)(filenames);
|
|
49
|
+
scanSpinner.succeed(`Found ${filesChanged.length} staged file(s).`);
|
|
50
|
+
console.log(chalk_1.default.dim('Files changed:'));
|
|
51
|
+
filesChanged.forEach((f) => console.log(chalk_1.default.green(` + ${f.file}`)));
|
|
52
|
+
console.log('');
|
|
53
|
+
// 5️⃣ Analyze diff
|
|
54
|
+
const analyzeSpinner = (0, ora_1.default)('Analyzing changes and checking branches...').start();
|
|
55
|
+
const diffs = await (0, git_1.getStagedDiff)(filenames);
|
|
56
|
+
const diffSummary = (0, analyzer_1.analyzeDiff)(diffs);
|
|
57
|
+
await (0, git_1.gitFetch)();
|
|
58
|
+
const allBranches = await (0, git_1.getAllBranches)();
|
|
59
|
+
const branchSummary = (0, compressBranchSummary_1.compressBranchSummary)(diffSummary);
|
|
60
|
+
const currentBranch = await (0, git_1.getCurrentBranchName)();
|
|
61
|
+
analyzeSpinner.succeed('Analysis complete.\n');
|
|
62
|
+
// 6️⃣ Commit Message Loop
|
|
63
|
+
let commitMessage = '';
|
|
64
|
+
let commitAccepted = false;
|
|
65
|
+
while (!commitAccepted) {
|
|
66
|
+
const commitSpinner = (0, ora_1.default)('Generating commit message...').start();
|
|
67
|
+
const prompt = (0, prompts_1.buildCommitPromptGemma)(diffSummary);
|
|
68
|
+
// const prompt = buildCommitPrompt(diffSummary);
|
|
69
|
+
commitMessage = await (0, ollama_1.generateWithGemma)(prompt);
|
|
70
|
+
// const response = await ai.models.generateContent({
|
|
71
|
+
// model: 'gemini-2.5-flash',
|
|
72
|
+
// contents: prompt,
|
|
73
|
+
// });
|
|
74
|
+
// console.log(response.text);
|
|
75
|
+
// commitMessage = (responseCommit.text || '').trim();
|
|
76
|
+
// commitMessage = response.text || '';
|
|
77
|
+
commitSpinner.succeed('Commit message generated:\n');
|
|
78
|
+
console.log(chalk_1.default.cyan(commitMessage));
|
|
79
|
+
console.log('');
|
|
80
|
+
const { action } = await inquirer_1.default.prompt([
|
|
81
|
+
{
|
|
82
|
+
type: 'list',
|
|
83
|
+
name: 'action',
|
|
84
|
+
message: 'What would you like to do with this commit message?',
|
|
85
|
+
choices: ['Continue', 'Edit', 'Retry'],
|
|
86
|
+
},
|
|
87
|
+
]);
|
|
88
|
+
if (action === 'Continue') {
|
|
89
|
+
commitAccepted = true;
|
|
90
|
+
}
|
|
91
|
+
else if (action === 'Edit') {
|
|
92
|
+
const promptInput = new enquirer_1.Input({
|
|
93
|
+
message: 'Edit your commit message:',
|
|
94
|
+
initial: commitMessage,
|
|
95
|
+
});
|
|
96
|
+
commitMessage = (await promptInput.run());
|
|
97
|
+
commitAccepted = true;
|
|
98
|
+
}
|
|
99
|
+
else if (action === 'Retry') {
|
|
100
|
+
console.log(chalk_1.default.yellow('Retrying commit message...'));
|
|
101
|
+
console.log('');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// 7️⃣ Commit
|
|
105
|
+
const commitProcessSpinner = (0, ora_1.default)('Committing changes...').start();
|
|
106
|
+
await (0, git_1.gitCommit)(commitMessage);
|
|
107
|
+
commitProcessSpinner.succeed('Changes successfully committed!\n');
|
|
108
|
+
// 8️⃣ Branch Name Loop
|
|
109
|
+
let branchName = '';
|
|
110
|
+
let branchAccepted = false;
|
|
111
|
+
while (!branchAccepted) {
|
|
112
|
+
const branchSpinner = (0, ora_1.default)('Generating branch name...').start();
|
|
113
|
+
const branchNamePrompt = (0, prompts_1.buildBranchPromptGemma)(branchSummary, allBranches, currentBranch, commitMessage);
|
|
114
|
+
const responseBranch = await (0, ollama_1.generateWithGemma)(branchNamePrompt);
|
|
115
|
+
branchName = (responseBranch.text || '').trim().replace(/['"]/g, '');
|
|
116
|
+
branchSpinner.succeed('Branch name generated:\n');
|
|
117
|
+
console.log(chalk_1.default.magenta(branchName));
|
|
118
|
+
console.log('');
|
|
119
|
+
const { action } = await inquirer_1.default.prompt([
|
|
120
|
+
{
|
|
121
|
+
type: 'list',
|
|
122
|
+
name: 'action',
|
|
123
|
+
message: 'What would you like to do with this branch name?',
|
|
124
|
+
choices: ['Continue', 'Edit', 'Retry'],
|
|
125
|
+
},
|
|
126
|
+
]);
|
|
127
|
+
if (action === 'Continue') {
|
|
128
|
+
branchAccepted = true;
|
|
129
|
+
}
|
|
130
|
+
else if (action === 'Edit') {
|
|
131
|
+
const promptInput = new enquirer_1.Input({
|
|
132
|
+
message: 'Edit your branch name:',
|
|
133
|
+
initial: branchName,
|
|
134
|
+
});
|
|
135
|
+
branchName = (await promptInput.run());
|
|
136
|
+
branchAccepted = true;
|
|
137
|
+
}
|
|
138
|
+
else if (action === 'Retry') {
|
|
139
|
+
console.log(chalk_1.default.yellow('Retrying branch name...'));
|
|
140
|
+
console.log('');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// 9️⃣ Rename Branch
|
|
144
|
+
const branchProcessSpinner = (0, ora_1.default)('Applying branch name...').start();
|
|
145
|
+
await (0, git_1.gitRenameBranch)(branchName);
|
|
146
|
+
branchProcessSpinner.succeed(`Branch correctly renamed to ${chalk_1.default.bold(branchName)}!`);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
if (err.name === 'ExitPromptError') {
|
|
150
|
+
console.log(chalk_1.default.yellow('\nProcess aborted using user prompt.\n'));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
(0, helper_1.log)(chalk_1.default.red(`We ran into an error: ${err}`));
|
|
154
|
+
}
|
|
155
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const ora_1 = __importDefault(require("ora"));
|
|
7
|
+
const startWorkflow_1 = __importDefault(require("./git/startWorkflow"));
|
|
8
|
+
const git_1 = require("../utils/git");
|
|
9
|
+
exports.default = async (payload = [], flags = {}) => {
|
|
10
|
+
const stageSpinner = (0, ora_1.default)('Staging files...').start();
|
|
11
|
+
if (payload.length > 0) {
|
|
12
|
+
await (0, git_1.stageFiles)(payload);
|
|
13
|
+
stageSpinner.succeed(`Staged ${payload.length} specified file(s).`);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
await (0, git_1.stageAll)();
|
|
17
|
+
stageSpinner.succeed('Staged all changes.');
|
|
18
|
+
}
|
|
19
|
+
console.log('');
|
|
20
|
+
await (0, startWorkflow_1.default)(flags);
|
|
21
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
7
|
+
const helper_1 = require("../../utils/helper");
|
|
8
|
+
exports.default = () => {
|
|
9
|
+
if ((0, helper_1.deleteApiKey)()) {
|
|
10
|
+
(0, helper_1.log)(chalk_1.default.green('API Key Deleted'));
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
(0, helper_1.log)(chalk_1.default.red('API Key Could Not Be Deleted. API KEY NOT FOUND!'));
|
|
14
|
+
}
|
|
15
|
+
};
|