git-aic 1.0.0 → 1.2.0
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/LICENSE +15 -0
- package/README.md +314 -0
- package/dist/cli.js +141 -13
- package/dist/config.js +61 -0
- package/dist/confirm.js +14 -13
- package/dist/editor.js +83 -0
- package/dist/git.js +36 -0
- package/dist/llm.js +8 -5
- package/dist/prompt.js +8 -2
- package/package.json +5 -8
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Spectra
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# Git Aic
|
|
2
|
+
|
|
3
|
+
**git-aic** is a command-line interface (CLI) tool built in TypeScript that upgrades your Git workflow by automatically generating high-quality, conventional commit messages.
|
|
4
|
+
|
|
5
|
+
Powered by Google Gemini, it analyzes your staged code changes and produces concise, descriptive, and standard-compliant commit messages, helping you maintain a clean and consistent Git history.
|
|
6
|
+
|
|
7
|
+
You define the rules.
|
|
8
|
+
You customize the system prompt.
|
|
9
|
+
You decide when it runs.
|
|
10
|
+
|
|
11
|
+
Your workflow. Your control.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **AI-Powered Message Generation**
|
|
16
|
+
Uses Google Gemini API to generate commit messages from your Git diff.
|
|
17
|
+
|
|
18
|
+
- **Self-Hosted & On-Demand**
|
|
19
|
+
Runs locally in your terminal. No background processes. No editor lock-in.
|
|
20
|
+
|
|
21
|
+
- **Full Control Over Rules**
|
|
22
|
+
Modify the system prompt to enforce your own commit conventions and formatting style.
|
|
23
|
+
|
|
24
|
+
- **Flexible Prompt Management**
|
|
25
|
+
Edit prompts globally or per repository, and set them from your editor, direct text, or a file.
|
|
26
|
+
|
|
27
|
+
- **Conventional Commits Compliance**
|
|
28
|
+
Strictly follows formats like `feat:`, `fix:`, `refactor:`, `chore:`.
|
|
29
|
+
|
|
30
|
+
- **Commit Confirmation & Editing**
|
|
31
|
+
Before committing, you can:
|
|
32
|
+
- Accept the suggested commit message
|
|
33
|
+
- Edit the full message in your editor
|
|
34
|
+
- Reject it
|
|
35
|
+
- Retry generation
|
|
36
|
+
|
|
37
|
+
- **Issue Linking**
|
|
38
|
+
Attach commits to GitHub issues with `--issue <number>`.
|
|
39
|
+
|
|
40
|
+
- **Optional Push After Commit**
|
|
41
|
+
Use `-p` or `--push` to push after committing.
|
|
42
|
+
|
|
43
|
+
- **Config Management**
|
|
44
|
+
Set your Gemini API key or view your config:
|
|
45
|
+
- `git aic config --key <key>`
|
|
46
|
+
- `git aic config`
|
|
47
|
+
|
|
48
|
+
- **TypeScript & Type Safety**
|
|
49
|
+
Built with TypeScript for maintainability and reliability.
|
|
50
|
+
|
|
51
|
+
- **Seamless Git Integration**
|
|
52
|
+
Directly integrates with Git using a CLI.
|
|
53
|
+
|
|
54
|
+
## Why Not Just Use Copilot?
|
|
55
|
+
|
|
56
|
+
Many AI commit tools:
|
|
57
|
+
|
|
58
|
+
- Depend on editor integrations
|
|
59
|
+
- Limit customization
|
|
60
|
+
- Enforce their defaults
|
|
61
|
+
- Restrict usage
|
|
62
|
+
- Run continuously in the background
|
|
63
|
+
|
|
64
|
+
This tool is different.
|
|
65
|
+
|
|
66
|
+
It runs only when you call it.
|
|
67
|
+
It follows your prompt rules.
|
|
68
|
+
It generates commits exactly how you define them.
|
|
69
|
+
It stays out of your way.
|
|
70
|
+
|
|
71
|
+
There are no forced conventions.
|
|
72
|
+
No hidden behavior.
|
|
73
|
+
No unnecessary background processes.
|
|
74
|
+
|
|
75
|
+
If needed, you can rotate API keys later. You stay in control.
|
|
76
|
+
|
|
77
|
+
This is controlled automation — not passive AI assistance.
|
|
78
|
+
|
|
79
|
+
## User Installation
|
|
80
|
+
|
|
81
|
+
To install `git-aic` globally via npm:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npm i -g git-aic
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
git aic --help
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Developer Installation (For Contributors)
|
|
92
|
+
|
|
93
|
+
### 1. Clone the Repository
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
git clone https://github.com/Spectra010s/git-aic.git
|
|
97
|
+
cd git-aic
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 2. Install Dependencies
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npm install
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 3. Build the Project
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npm run build
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Configuration
|
|
113
|
+
|
|
114
|
+
### Set API Key (Primary)
|
|
115
|
+
|
|
116
|
+
Use the CLI config command to save your Google Gemini API key:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
git aic config --key <your_api_key>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
To view your current config:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
git aic config
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
> **Note:** It is masked by default for security reasons.
|
|
129
|
+
|
|
130
|
+
To view the full saved API key:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
git aic config --show
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Environment Variable (Fallback)
|
|
137
|
+
|
|
138
|
+
If you prefer not to use the config system, you can set it manually in your environment:
|
|
139
|
+
|
|
140
|
+
- **macOS / Linux:**
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
export GEMINI_COMMIT_MESSAGE_API_KEY=your_api_key_here
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- **Windows (PowerShell):**
|
|
147
|
+
|
|
148
|
+
```powershell
|
|
149
|
+
setx GEMINI_COMMIT_MESSAGE_API_KEY "your_api_key_here"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
After setting the variable, restart your terminal.
|
|
153
|
+
|
|
154
|
+
> **Note:** This method works, but using the CLI config is safer and easier for long-term usage.
|
|
155
|
+
|
|
156
|
+
## Usage
|
|
157
|
+
|
|
158
|
+
### Commit With AI Assistance
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
git aic
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- Prompts you with a generated commit message.
|
|
165
|
+
- You can **accept, edit, reject, or retry** the message.
|
|
166
|
+
- Editing opens your editor and supports multiline commit messages.
|
|
167
|
+
|
|
168
|
+
### Commit and Link to Issue
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
git aic --issue 123
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
- Attaches the commit to GitHub issue #123.
|
|
175
|
+
|
|
176
|
+
### Commit and Push
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
git aic -p
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- Pushes automatically after committing.
|
|
183
|
+
|
|
184
|
+
### Configure API Key
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
git aic config --key <key>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- Saves your Google Gemini API key.
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
git aic config
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- Displays your saved config.
|
|
197
|
+
|
|
198
|
+
### Manage Prompts
|
|
199
|
+
|
|
200
|
+
Edit the global prompt:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
git aic prompt edit
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Set the global prompt directly from text:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
git aic prompt edit --text "Write concise conventional commits with a short body when needed."
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Load the global prompt from a file:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
git aic prompt edit --file ./prompt.txt
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Reset the global prompt:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
git aic prompt reset
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Edit a repository-local prompt:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
git aic prompt edit --local
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Set a repository-local prompt from text:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
git aic prompt edit --local --text "Use a short subject and a clear explanatory body."
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Load a repository-local prompt from a file:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
git aic prompt edit --local --file ./commit-prompt.txt
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Reset the local prompt:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
git aic prompt reset --local
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Prompt resolution order:
|
|
249
|
+
|
|
250
|
+
1. local prompt from Git config
|
|
251
|
+
2. global prompt from Git-AIC config
|
|
252
|
+
3. built-in default prompt
|
|
253
|
+
|
|
254
|
+
## How It Works
|
|
255
|
+
|
|
256
|
+
1. Captures your staged Git diff
|
|
257
|
+
2. Builds a strict system prompt
|
|
258
|
+
3. Resolves the active prompt from local, global, or default settings
|
|
259
|
+
4. Sends the prompt and diff to Gemini
|
|
260
|
+
5. Prompts for commit confirmation (accept, edit, retry, reject)
|
|
261
|
+
6. Opens your editor when you choose to edit the generated message
|
|
262
|
+
7. Executes `git commit` automatically
|
|
263
|
+
8. Optionally pushes if `-p` flag is used
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Technologies Used
|
|
268
|
+
|
|
269
|
+
| Technology | Purpose |
|
|
270
|
+
| ----------------- | ---------------------- |
|
|
271
|
+
| TypeScript | Core language |
|
|
272
|
+
| Node.js | Runtime |
|
|
273
|
+
| Axios | HTTP client |
|
|
274
|
+
| Chalk | Styled terminal output |
|
|
275
|
+
| Commander.js | CLI framework |
|
|
276
|
+
| Simple-Git | Git integration |
|
|
277
|
+
| Google Gemini API | LLM text generation |
|
|
278
|
+
|
|
279
|
+
## Final Takeaway
|
|
280
|
+
|
|
281
|
+
Automating repetitive tasks like **commit messages** saves time — but the real win here is ownership.
|
|
282
|
+
|
|
283
|
+
git-aic:
|
|
284
|
+
|
|
285
|
+
- **Self-hosted** — runs entirely on your machine
|
|
286
|
+
- **On-demand** — only runs when you call it
|
|
287
|
+
- **Fully customizable** — prompts, commit format, workflow
|
|
288
|
+
- **Under your control** — you decide every step
|
|
289
|
+
|
|
290
|
+
It runs when you need it, follows your rules, and generates commits the way **you** want.
|
|
291
|
+
|
|
292
|
+
_Choose your model. Define your prompt. Control the format. Extend or optimize anytime._
|
|
293
|
+
|
|
294
|
+
Instead of adapting to someone else's defaults, _you built a system tailored to your workflow._
|
|
295
|
+
|
|
296
|
+
You are not just using AI tools.
|
|
297
|
+
You are **building them to fit your process.**
|
|
298
|
+
|
|
299
|
+
## License
|
|
300
|
+
|
|
301
|
+
[**ISC License**](https://github.com/Spectra010s/git-aic/blob/main/LICENSE)
|
|
302
|
+
|
|
303
|
+
## Author
|
|
304
|
+
|
|
305
|
+
Spectra010s
|
|
306
|
+
|
|
307
|
+
- [Twitter](https://x.com/Spectra010s)
|
|
308
|
+
- [LinkedIn](https://www.linkedin.com/in/adeloye-adetayo-273723253)
|
|
309
|
+
|
|
310
|
+
## Parent Repository
|
|
311
|
+
|
|
312
|
+
This project is a fork and standalone version of:
|
|
313
|
+
|
|
314
|
+
[https://github.com/samueltuoyo15/Commit-Message-Tool](https://github.com/samueltuoyo15/Commit-Message-Tool)
|
package/dist/cli.js
CHANGED
|
@@ -1,19 +1,138 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs from "fs/promises";
|
|
2
3
|
import { Command } from "commander";
|
|
3
4
|
import { simpleGit } from "simple-git";
|
|
4
5
|
import chalk from "chalk";
|
|
5
|
-
import { getGitDiff } from "./git.js";
|
|
6
|
+
import { ensureInsideGitRepo, getGitDiff, getLocalPrompt, resetLocalPrompt, setLocalPrompt, } from "./git.js";
|
|
6
7
|
import { generateCommitMessage } from "./llm.js";
|
|
7
8
|
import { getUserConfirmation } from "./confirm.js";
|
|
9
|
+
import { editPromptInEditor } from "./editor.js";
|
|
10
|
+
import { getConfig, resetCustomPrompt, setApiKey, setCustomPrompt, } from "./config.js";
|
|
11
|
+
import { DEFAULT_SYSTEM_PROMPT } from "./prompt.js";
|
|
12
|
+
process.on("SIGINT", () => {
|
|
13
|
+
process.exit(0);
|
|
14
|
+
});
|
|
8
15
|
const git = simpleGit();
|
|
9
16
|
const program = new Command();
|
|
17
|
+
function getErrorMessage(error) {
|
|
18
|
+
return error instanceof Error ? error.message : String(error);
|
|
19
|
+
}
|
|
10
20
|
program
|
|
11
|
-
.name("
|
|
21
|
+
.name("git aic")
|
|
12
22
|
.description("AI-powered Git commit generator using Google Gemini")
|
|
13
23
|
.version("1.0.0")
|
|
14
|
-
.option("-p, --push", "push after committing")
|
|
24
|
+
.option("-p, --push", "push after committing")
|
|
25
|
+
.option("-i, --issue <number>", "Link commit to GitHub issue");
|
|
26
|
+
program
|
|
27
|
+
.command("config")
|
|
28
|
+
.description("Configure the Gemini API Key settings")
|
|
29
|
+
.option("-k, --key <key>", "Set your Gemini API Key")
|
|
30
|
+
.option("--show", "Show the full API key")
|
|
31
|
+
.action(async (options) => {
|
|
32
|
+
const cfg = await getConfig();
|
|
33
|
+
if (options.key) {
|
|
34
|
+
await setApiKey(options.key);
|
|
35
|
+
console.log(chalk.green("API Key saved successfully!"));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (cfg.apiKey) {
|
|
39
|
+
if (options.show) {
|
|
40
|
+
console.log(chalk.green("Current API Key:"), cfg.apiKey);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const styled = cfg.apiKey.slice(0, 4) +
|
|
44
|
+
chalk.dim("*".repeat(cfg.apiKey.length - 8)) +
|
|
45
|
+
cfg.apiKey.slice(-4);
|
|
46
|
+
console.log(chalk.green("Current API Key:"), styled);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log(chalk.yellow("No API key set"));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const promptCommand = program
|
|
54
|
+
.command("prompt")
|
|
55
|
+
.description("Manage the system prompt used for commit generation");
|
|
56
|
+
promptCommand
|
|
57
|
+
.command("edit")
|
|
58
|
+
.description("Edit and save a custom system prompt in your editor")
|
|
59
|
+
.option("--local", "Save the custom prompt in the current repository")
|
|
60
|
+
.option("--global", "Save the custom prompt in the global git-aic config")
|
|
61
|
+
.option("-t, --text <prompt>", "Set the custom prompt from a string")
|
|
62
|
+
.option("-f, --file <path>", "Set the custom prompt from a file")
|
|
63
|
+
.action(async (options) => {
|
|
64
|
+
try {
|
|
65
|
+
const cfg = await getConfig();
|
|
66
|
+
if (options.local && options.global) {
|
|
67
|
+
console.log(chalk.red("Use either --local or --global, not both at the same time."));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
if (options.text && options.file) {
|
|
71
|
+
console.log(chalk.red("Use either --text or --file, not both at the same time."));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
if (options.local) {
|
|
75
|
+
await ensureInsideGitRepo();
|
|
76
|
+
}
|
|
77
|
+
let editedPrompt = "";
|
|
78
|
+
if (options.text) {
|
|
79
|
+
editedPrompt = options.text.trim();
|
|
80
|
+
}
|
|
81
|
+
else if (options.file) {
|
|
82
|
+
editedPrompt = (await fs.readFile(options.file, "utf-8")).trim();
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const startingPrompt = options.local
|
|
86
|
+
? ((await getLocalPrompt()) ?? cfg.customPrompt ?? DEFAULT_SYSTEM_PROMPT)
|
|
87
|
+
: (cfg.customPrompt ?? DEFAULT_SYSTEM_PROMPT);
|
|
88
|
+
editedPrompt = await editPromptInEditor(startingPrompt);
|
|
89
|
+
}
|
|
90
|
+
if (!editedPrompt) {
|
|
91
|
+
console.log(chalk.red("Prompt was empty. Nothing saved."));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
if (options.local) {
|
|
95
|
+
await setLocalPrompt(editedPrompt);
|
|
96
|
+
console.log(chalk.green("Local system prompt saved successfully!"));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
await setCustomPrompt(editedPrompt);
|
|
100
|
+
console.log(chalk.green("Global system prompt saved successfully!"));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.error(chalk.red("Prompt edit failed:"), getErrorMessage(error));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
promptCommand
|
|
109
|
+
.command("reset")
|
|
110
|
+
.description("Reset the system prompt back to the default")
|
|
111
|
+
.option("--local", "Reset the prompt in the current repository")
|
|
112
|
+
.option("--global", "Reset the prompt in the global git-aic config")
|
|
113
|
+
.action(async (options) => {
|
|
114
|
+
try {
|
|
115
|
+
if (options.local && options.global) {
|
|
116
|
+
console.log(chalk.red("Use either --local or --global, not both at the same time."));
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
if (options.local) {
|
|
120
|
+
await ensureInsideGitRepo();
|
|
121
|
+
await resetLocalPrompt();
|
|
122
|
+
console.log(chalk.green("Local system prompt reset to default."));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
await resetCustomPrompt();
|
|
126
|
+
console.log(chalk.green("Global system prompt reset to default."));
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.error(chalk.red("Prompt reset failed:"), getErrorMessage(error));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
15
133
|
program.action(async (options) => {
|
|
16
134
|
try {
|
|
135
|
+
await ensureInsideGitRepo();
|
|
17
136
|
const diff = await getGitDiff();
|
|
18
137
|
if (!diff) {
|
|
19
138
|
console.log(chalk.yellow("No changes to commit!"));
|
|
@@ -23,17 +142,20 @@ program.action(async (options) => {
|
|
|
23
142
|
console.log(chalk.blue("\nFiles being committed:"));
|
|
24
143
|
status.staged.forEach((file) => console.log(chalk.cyan(`- ${file}`)));
|
|
25
144
|
console.log("");
|
|
26
|
-
let
|
|
145
|
+
let currentMsg = "";
|
|
27
146
|
let confirmed = false;
|
|
147
|
+
let finalMsg = "";
|
|
148
|
+
const issueSuffix = options.issue ? `, closes #${options.issue}` : "";
|
|
28
149
|
console.log(chalk.blue("Analyzing staged changes...\n"));
|
|
29
150
|
while (!confirmed) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
151
|
+
let currentMsg = await generateCommitMessage(diff);
|
|
152
|
+
currentMsg = `${currentMsg}${issueSuffix}`;
|
|
153
|
+
const { choice, message } = await getUserConfirmation(currentMsg);
|
|
154
|
+
if (choice === "y") {
|
|
155
|
+
finalMsg = message;
|
|
34
156
|
confirmed = true;
|
|
35
157
|
}
|
|
36
|
-
else if (choice ===
|
|
158
|
+
else if (choice === "r") {
|
|
37
159
|
console.log(chalk.yellow("Regenerating...\n"));
|
|
38
160
|
continue;
|
|
39
161
|
}
|
|
@@ -42,8 +164,8 @@ program.action(async (options) => {
|
|
|
42
164
|
process.exit(0);
|
|
43
165
|
}
|
|
44
166
|
}
|
|
45
|
-
console.log(chalk.blue(`> ran: git commit -m "${
|
|
46
|
-
await git.commit(
|
|
167
|
+
console.log(chalk.blue(`> ran: git commit -m "${finalMsg}"`));
|
|
168
|
+
await git.commit(finalMsg);
|
|
47
169
|
console.log(chalk.green("\nCommit successful"));
|
|
48
170
|
if (options.push) {
|
|
49
171
|
console.log(chalk.blue("> ran: git push"));
|
|
@@ -52,8 +174,14 @@ program.action(async (options) => {
|
|
|
52
174
|
}
|
|
53
175
|
}
|
|
54
176
|
catch (error) {
|
|
55
|
-
|
|
56
|
-
|
|
177
|
+
let isAbort = false;
|
|
178
|
+
if (typeof error === "object" && error !== null && "code" in error) {
|
|
179
|
+
const e = error;
|
|
180
|
+
isAbort = e.code === "ABORT_ERR";
|
|
181
|
+
}
|
|
182
|
+
const msg = isAbort ? "Operation cancelled" : getErrorMessage(error);
|
|
183
|
+
console.error(chalk.red("\nCommit failed:"), msg);
|
|
184
|
+
process.exit(isAbort ? 0 : 1);
|
|
57
185
|
}
|
|
58
186
|
});
|
|
59
187
|
program.parse(process.argv);
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { getLocalPrompt } from "./git.js";
|
|
6
|
+
function getConfigPath() {
|
|
7
|
+
const toolName = "git-aic";
|
|
8
|
+
if (process.platform === "win32") {
|
|
9
|
+
const appData = process.env.APPDATA;
|
|
10
|
+
if (!appData)
|
|
11
|
+
throw new Error("APPDATA not defined");
|
|
12
|
+
return path.join(appData, toolName, "config.json");
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
const configDir = path.join(os.homedir(), ".config", toolName);
|
|
16
|
+
return path.join(configDir, "config.json");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function getConfig() {
|
|
20
|
+
const configPath = getConfigPath();
|
|
21
|
+
try {
|
|
22
|
+
const content = await fs.readFile(configPath, "utf-8");
|
|
23
|
+
return JSON.parse(content);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
if (err.code === "ENOENT")
|
|
27
|
+
return {};
|
|
28
|
+
console.error(chalk.red("Failed to read config:"), err.message);
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function saveConfig(data) {
|
|
33
|
+
const configPath = getConfigPath();
|
|
34
|
+
const dir = path.dirname(configPath);
|
|
35
|
+
await fs.mkdir(dir, { recursive: true });
|
|
36
|
+
const current = await getConfig();
|
|
37
|
+
const newConfig = { ...current, ...data };
|
|
38
|
+
await fs.writeFile(configPath, JSON.stringify(newConfig, null, 2), "utf-8");
|
|
39
|
+
}
|
|
40
|
+
export async function setApiKey(key) {
|
|
41
|
+
await saveConfig({ apiKey: key });
|
|
42
|
+
}
|
|
43
|
+
export async function setCustomPrompt(prompt) {
|
|
44
|
+
await saveConfig({ customPrompt: prompt });
|
|
45
|
+
}
|
|
46
|
+
export async function resetCustomPrompt() {
|
|
47
|
+
const { customPrompt, ...rest } = await getConfig();
|
|
48
|
+
void customPrompt;
|
|
49
|
+
const configPath = getConfigPath();
|
|
50
|
+
const dir = path.dirname(configPath);
|
|
51
|
+
await fs.mkdir(dir, { recursive: true });
|
|
52
|
+
await fs.writeFile(configPath, JSON.stringify(rest, null, 2), "utf-8");
|
|
53
|
+
}
|
|
54
|
+
export async function getResolvedPrompt() {
|
|
55
|
+
const localPrompt = await getLocalPrompt();
|
|
56
|
+
if (localPrompt) {
|
|
57
|
+
return localPrompt;
|
|
58
|
+
}
|
|
59
|
+
const config = await getConfig();
|
|
60
|
+
return config.customPrompt;
|
|
61
|
+
}
|
package/dist/confirm.js
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
import readline from "node:readline/promises";
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
+
import { editTextInEditor } from "./editor.js";
|
|
3
4
|
export const getUserConfirmation = async (message) => {
|
|
4
5
|
const rl = readline.createInterface({
|
|
5
6
|
input: process.stdin,
|
|
6
7
|
output: process.stdout,
|
|
7
8
|
});
|
|
8
9
|
console.log(chalk.green(`\nProposed: "${message}"`));
|
|
9
|
-
const answer = await rl.question(chalk.blue(
|
|
10
|
+
const answer = await rl.question(chalk.blue("Confirm commit? [y=yes, n=no, r=retry, e=edit]: "));
|
|
10
11
|
rl.close();
|
|
11
|
-
const choice = (answer.toLowerCase() ||
|
|
12
|
-
if (choice ===
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
const choice = (answer.toLowerCase() || "y").trim();
|
|
13
|
+
if (choice === "e") {
|
|
14
|
+
let editedMessage = message;
|
|
15
|
+
while (true) {
|
|
16
|
+
editedMessage = await editTextInEditor(editedMessage, "git-aic-commit-", "COMMIT_EDITMSG");
|
|
17
|
+
if (editedMessage.trim()) {
|
|
18
|
+
return { choice: "y", message: editedMessage };
|
|
19
|
+
}
|
|
20
|
+
if (!editedMessage.trim()) {
|
|
21
|
+
console.log(chalk.red("Commit message cannot be empty. Please edit it again."));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
23
24
|
}
|
|
24
25
|
return { choice, message };
|
|
25
26
|
};
|
package/dist/editor.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
function splitCommand(command) {
|
|
6
|
+
const parts = [];
|
|
7
|
+
let current = "";
|
|
8
|
+
let quote = null;
|
|
9
|
+
let escaping = false;
|
|
10
|
+
for (const char of command.trim()) {
|
|
11
|
+
if (escaping) {
|
|
12
|
+
current += char;
|
|
13
|
+
escaping = false;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (char === "\\") {
|
|
17
|
+
escaping = true;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (quote) {
|
|
21
|
+
if (char === quote) {
|
|
22
|
+
quote = null;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
current += char;
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (char === '"' || char === "'") {
|
|
30
|
+
quote = char;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (/\s/.test(char)) {
|
|
34
|
+
if (current) {
|
|
35
|
+
parts.push(current);
|
|
36
|
+
current = "";
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
current += char;
|
|
41
|
+
}
|
|
42
|
+
if (escaping || quote) {
|
|
43
|
+
throw new Error("Editor command has invalid quoting.");
|
|
44
|
+
}
|
|
45
|
+
if (current) {
|
|
46
|
+
parts.push(current);
|
|
47
|
+
}
|
|
48
|
+
return parts;
|
|
49
|
+
}
|
|
50
|
+
export function openInEditor(filePath) {
|
|
51
|
+
const editor = process.env.VISUAL || process.env.EDITOR || "editor";
|
|
52
|
+
const [editorCommand, ...editorArgs] = splitCommand(editor);
|
|
53
|
+
if (!editorCommand) {
|
|
54
|
+
throw new Error("No editor found. Set VISUAL or EDITOR, or install an `editor` command.");
|
|
55
|
+
}
|
|
56
|
+
const result = spawnSync(editorCommand, [...editorArgs, filePath], {
|
|
57
|
+
stdio: "inherit",
|
|
58
|
+
});
|
|
59
|
+
if (result.error) {
|
|
60
|
+
if ("code" in result.error && result.error.code === "ENOENT") {
|
|
61
|
+
throw new Error("No editor found. Set VISUAL or EDITOR, or install an `editor` command.");
|
|
62
|
+
}
|
|
63
|
+
throw result.error;
|
|
64
|
+
}
|
|
65
|
+
if (result.status !== 0) {
|
|
66
|
+
throw new Error(`Editor exited with status ${result.status}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function editTextInEditor(initialText, prefix = "git-aic-", fileName = "message.txt") {
|
|
70
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
71
|
+
const tempFile = path.join(tempDir, fileName);
|
|
72
|
+
try {
|
|
73
|
+
await fs.writeFile(tempFile, `${initialText}\n`, "utf-8");
|
|
74
|
+
openInEditor(tempFile);
|
|
75
|
+
return (await fs.readFile(tempFile, "utf-8")).trim();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export async function editPromptInEditor(initialPrompt) {
|
|
82
|
+
return editTextInEditor(initialPrompt, "git-aic-prompt-", "prompt.txt");
|
|
83
|
+
}
|
package/dist/git.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { simpleGit } from "simple-git";
|
|
2
2
|
const git = simpleGit();
|
|
3
|
+
const LOCAL_PROMPT_KEY = "aic.prompt";
|
|
4
|
+
export async function isInsideGitRepo() {
|
|
5
|
+
try {
|
|
6
|
+
const result = await git.raw(["rev-parse", "--is-inside-work-tree"]);
|
|
7
|
+
return result.trim() === "true";
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export async function ensureInsideGitRepo() {
|
|
14
|
+
if (!(await isInsideGitRepo())) {
|
|
15
|
+
throw new Error("Not inside a Git repository.");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
3
18
|
export const getGitDiff = async () => {
|
|
4
19
|
try {
|
|
5
20
|
await git.raw(["config", "core.autocrlf", "true"]);
|
|
@@ -18,3 +33,24 @@ export const getGitDiff = async () => {
|
|
|
18
33
|
return "";
|
|
19
34
|
}
|
|
20
35
|
};
|
|
36
|
+
export async function getLocalPrompt() {
|
|
37
|
+
try {
|
|
38
|
+
const prompt = await git.raw(["config", "--local", "--get", LOCAL_PROMPT_KEY]);
|
|
39
|
+
const trimmed = prompt.trim();
|
|
40
|
+
return trimmed || undefined;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function setLocalPrompt(prompt) {
|
|
47
|
+
await git.raw(["config", "--local", LOCAL_PROMPT_KEY, prompt]);
|
|
48
|
+
}
|
|
49
|
+
export async function resetLocalPrompt() {
|
|
50
|
+
try {
|
|
51
|
+
await git.raw(["config", "--local", "--unset", LOCAL_PROMPT_KEY]);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
package/dist/llm.js
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { buildPrompt } from "./prompt.js";
|
|
4
|
+
import { getConfig, getResolvedPrompt } from "./config.js";
|
|
4
5
|
export const generateCommitMessage = async (rawDiff) => {
|
|
5
6
|
const API_URL = "https://generativelanguage.googleapis.com/v1/models/gemini-2.5-flash:generateContent";
|
|
6
|
-
const
|
|
7
|
+
const config = await getConfig();
|
|
8
|
+
const API_KEY = config.apiKey || process.env.GEMINI_COMMIT_MESSAGE_API_KEY;
|
|
7
9
|
if (!API_KEY) {
|
|
8
10
|
console.error(chalk.red("\nMissing GEMINI_COMMIT_MESSAGE_API_KEY environment variable.\n"));
|
|
9
11
|
console.log("Please set your API key before running this command.\n");
|
|
10
12
|
console.log(chalk.yellow("How to fix this:\n"));
|
|
11
|
-
console.log(chalk.
|
|
13
|
+
console.log(chalk.gray("Recommended: use the config helper to save it permanently:\n git-aic config --key <your_api_key>\n"));
|
|
14
|
+
console.log(chalk.cyan("macOS / Linux (temporary):"));
|
|
12
15
|
console.log(" export GEMINI_COMMIT_MESSAGE_API_KEY=your_api_key_here\n");
|
|
13
|
-
console.log(chalk.cyan("Windows (PowerShell):"));
|
|
16
|
+
console.log(chalk.cyan("Windows (PowerShell, temporary):"));
|
|
14
17
|
console.log(' setx GEMINI_COMMIT_MESSAGE_API_KEY "your_api_key_here"\n');
|
|
15
|
-
console.log(chalk.gray("After setting the
|
|
18
|
+
console.log(chalk.gray("After setting the key, restart your terminal.\n"));
|
|
16
19
|
process.exit(1);
|
|
17
20
|
}
|
|
18
|
-
const prompt = buildPrompt(rawDiff);
|
|
21
|
+
const prompt = buildPrompt(rawDiff, await getResolvedPrompt());
|
|
19
22
|
try {
|
|
20
23
|
const response = await axios.post(API_URL, {
|
|
21
24
|
contents: [{ parts: [{ text: prompt }] }],
|
package/dist/prompt.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const DEFAULT_SYSTEM_PROMPT = `
|
|
2
2
|
CRITICAL INSTRUCTIONS - READ CAREFULLY:
|
|
3
3
|
You are an expert Git commit message writer. You MUST follow ALL these rules:
|
|
4
4
|
|
|
@@ -38,7 +38,13 @@ YOUR TASK:
|
|
|
38
38
|
Analyze this git diff and generate exactly ONE proper commit message following all rules above.
|
|
39
39
|
|
|
40
40
|
Git diff:
|
|
41
|
-
|
|
41
|
+
{{diff}}
|
|
42
42
|
|
|
43
43
|
Commit message:
|
|
44
44
|
`.trim();
|
|
45
|
+
export const buildPrompt = (diff, systemPrompt = DEFAULT_SYSTEM_PROMPT) => {
|
|
46
|
+
if (systemPrompt.includes("{{diff}}")) {
|
|
47
|
+
return systemPrompt.split("{{diff}}").join(diff);
|
|
48
|
+
}
|
|
49
|
+
return `${systemPrompt.trim()}\n\nGit diff:\n${diff}\n\nCommit message:`.trim();
|
|
50
|
+
};
|
package/package.json
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-aic",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "AI-powered Git commit generator using Google Gemini",
|
|
5
|
-
"homepage": "https://github.com/
|
|
5
|
+
"homepage": "https://github.com/Spectra010s/git-aic",
|
|
6
6
|
"bugs": {
|
|
7
|
-
"url": "https://github.com/
|
|
7
|
+
"url": "https://github.com/Spectra010s/git-aic/issues"
|
|
8
8
|
},
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/
|
|
11
|
+
"url": "git+https://github.com/Spectra010s/git-aic.git"
|
|
12
12
|
},
|
|
13
13
|
"license": "ISC",
|
|
14
14
|
"author": "Spectra010s",
|
|
15
15
|
"type": "module",
|
|
16
16
|
"main": "./dist/cli.js",
|
|
17
17
|
"bin": {
|
|
18
|
-
"git-aic": "
|
|
18
|
+
"git-aic": "dist/cli.js"
|
|
19
19
|
},
|
|
20
|
-
"files": [
|
|
21
|
-
"dist"
|
|
22
|
-
],
|
|
23
20
|
"scripts": {
|
|
24
21
|
"build": "tsc",
|
|
25
22
|
"prepare": "npm run build"
|