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 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("commit")
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 currentMessage = "";
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
- currentMessage = await generateCommitMessage(diff);
31
- const { choice, message } = await getUserConfirmation(currentMessage);
32
- if (choice === 'y') {
33
- currentMessage = message;
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 === 'r') {
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 "${currentMessage}"`));
46
- await git.commit(currentMessage);
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
- console.error(chalk.red("\nCommit failed:"), error);
56
- process.exit(1);
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('Confirm commit? [y=yes, n=no, r=retry, e=edit]: '));
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() || 'y').trim();
12
- if (choice === 'e') {
13
- const editRl = readline.createInterface({
14
- input: process.stdin,
15
- output: process.stdout,
16
- terminal: true
17
- });
18
- console.log(chalk.cyan("\nEdit the message:"));
19
- editRl.write(message);
20
- const editedMessage = await editRl.question("> ");
21
- editRl.close();
22
- return { choice: 'y', message: editedMessage };
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 API_KEY = process.env.GEMINI_COMMIT_MESSAGE_API_KEY;
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.cyan("macOS / Linux:"));
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 variable, restart your terminal.\n"));
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 buildPrompt = (diff) => `
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
- ${diff}
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.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "AI-powered Git commit generator using Google Gemini",
5
- "homepage": "https://github.com/samueltuoyo15/Commit-Message-Tool/tree/#readme",
5
+ "homepage": "https://github.com/Spectra010s/git-aic",
6
6
  "bugs": {
7
- "url": "https://github.com/samueltuoyo15/Commit-Message-Tool/issues"
7
+ "url": "https://github.com/Spectra010s/git-aic/issues"
8
8
  },
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+https://github.com/samueltuoyo15/Commit-Message-Tool.git"
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": "./dist/cli.js"
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"