xoegit 1.0.1 → 1.1.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/README.md +8 -5
- package/dist/cli/analyze.js +5 -9
- package/dist/config/service.js +2 -2
- package/dist/git/service.js +3 -3
- package/dist/prompts/service.js +1 -1
- package/dist/providers/gemini.js +19 -16
- package/dist/providers/models.js +2 -5
- package/dist/utils/input.js +1 -1
- package/package.json +29 -4
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# xoegit
|
|
2
2
|
|
|
3
|
-

|
|
5
|
-

|
|
6
|
-
](https://nodejs.org/)
|
|
4
|
+
[](https://www.typescriptlang.org/)
|
|
5
|
+
[](https://ai.google.dev/)
|
|
6
|
+
[](https://www.npmjs.com/package/xoegit)
|
|
7
|
+
[](https://github.com/ujangdoubleday/xoegit/blob/main/LICENSE.md)
|
|
7
8
|
|
|
8
9
|
**xoegit** is an AI-powered CLI tool that generates concise, semantic, and atomic git commit messages and PR descriptions. It analyzes your `git diff`, `git status`, and `git log` to provide context-aware suggestions powered by Google's Gemini models.
|
|
9
10
|
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
|
|
22
23
|
### Prerequisites
|
|
23
24
|
|
|
24
|
-
- **Node.js**: Version
|
|
25
|
+
- **Node.js**: Version 20.19.5 or higher
|
|
25
26
|
- **Git**: Must be installed and available in your PATH
|
|
26
27
|
- **API Key**: A Google Gemini API key ([get one here](https://aistudio.google.com/))
|
|
27
28
|
|
|
@@ -51,6 +52,8 @@ Simply run `xoegit` for the first time. It will prompt you for your API Key secu
|
|
|
51
52
|
xoegit
|
|
52
53
|
```
|
|
53
54
|
|
|
55
|
+

|
|
56
|
+
|
|
54
57
|
### Options
|
|
55
58
|
|
|
56
59
|
| Option | Description |
|
package/dist/cli/analyze.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ora from 'ora';
|
|
2
2
|
import { program } from './program.js';
|
|
3
3
|
import { ConfigService } from '../config/index.js';
|
|
4
|
-
import { isValidApiKey, promptApiKey, showBanner, showSuggestion, showSuccess, showError, showWarning, showInfo, showTip, spinnerText } from '../utils/index.js';
|
|
4
|
+
import { isValidApiKey, promptApiKey, showBanner, showSuggestion, showSuccess, showError, showWarning, showInfo, showTip, spinnerText, } from '../utils/index.js';
|
|
5
5
|
import { getGitDiff, getGitLog, getGitStatus, isGitRepository } from '../git/index.js';
|
|
6
6
|
import { generateSystemPrompt } from '../prompts/index.js';
|
|
7
7
|
import { generateCommitSuggestion } from '../providers/index.js';
|
|
@@ -25,7 +25,7 @@ export async function analyzeAction() {
|
|
|
25
25
|
try {
|
|
26
26
|
apiKey = await promptApiKey();
|
|
27
27
|
}
|
|
28
|
-
catch (
|
|
28
|
+
catch (_err) {
|
|
29
29
|
showError('Input Error', 'Failed to read input.');
|
|
30
30
|
process.exit(1);
|
|
31
31
|
}
|
|
@@ -45,13 +45,9 @@ export async function analyzeAction() {
|
|
|
45
45
|
// 2. Fetch Git Info
|
|
46
46
|
const spinner = ora({
|
|
47
47
|
text: spinnerText.analyzing,
|
|
48
|
-
spinner: 'dots12'
|
|
48
|
+
spinner: 'dots12',
|
|
49
49
|
}).start();
|
|
50
|
-
const [diff, status, log] = await Promise.all([
|
|
51
|
-
getGitDiff(),
|
|
52
|
-
getGitStatus(),
|
|
53
|
-
getGitLog()
|
|
54
|
-
]);
|
|
50
|
+
const [diff, status, log] = await Promise.all([getGitDiff(), getGitStatus(), getGitLog()]);
|
|
55
51
|
// Check if there are any changes
|
|
56
52
|
const hasDiff = diff && diff.trim() !== 'Unstaged Changes:\n\n\nStaged Changes:';
|
|
57
53
|
let hasUntracked = false;
|
|
@@ -59,7 +55,7 @@ export async function analyzeAction() {
|
|
|
59
55
|
const statusObj = JSON.parse(status);
|
|
60
56
|
hasUntracked = statusObj.not_added && statusObj.not_added.length > 0;
|
|
61
57
|
}
|
|
62
|
-
catch (
|
|
58
|
+
catch (_e) {
|
|
63
59
|
// failed to parse
|
|
64
60
|
}
|
|
65
61
|
if (!hasDiff && !hasUntracked) {
|
package/dist/config/service.js
CHANGED
|
@@ -20,7 +20,7 @@ export class ConfigService {
|
|
|
20
20
|
return config.XOEGIT_GEMINI_API_KEY;
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
-
catch (
|
|
23
|
+
catch (_error) {
|
|
24
24
|
// Config file doesn't exist or is invalid, ignore
|
|
25
25
|
}
|
|
26
26
|
return undefined;
|
|
@@ -34,7 +34,7 @@ export class ConfigService {
|
|
|
34
34
|
const existing = await fs.readFile(this.configPath, 'utf-8');
|
|
35
35
|
config = JSON.parse(existing);
|
|
36
36
|
}
|
|
37
|
-
catch (
|
|
37
|
+
catch (_e) {
|
|
38
38
|
// ignore
|
|
39
39
|
}
|
|
40
40
|
config.XOEGIT_GEMINI_API_KEY = apiKey;
|
package/dist/git/service.js
CHANGED
|
@@ -7,7 +7,7 @@ export async function isGitRepository() {
|
|
|
7
7
|
try {
|
|
8
8
|
return await git.checkIsRepo();
|
|
9
9
|
}
|
|
10
|
-
catch (
|
|
10
|
+
catch (_error) {
|
|
11
11
|
return false;
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -34,7 +34,7 @@ export async function getGitLog(maxCount = 5) {
|
|
|
34
34
|
const log = await git.log({ maxCount });
|
|
35
35
|
return JSON.stringify(log.all, null, 2);
|
|
36
36
|
}
|
|
37
|
-
catch (
|
|
38
|
-
return
|
|
37
|
+
catch (_error) {
|
|
38
|
+
return 'No commits yet.';
|
|
39
39
|
}
|
|
40
40
|
}
|
package/dist/prompts/service.js
CHANGED
|
@@ -11,7 +11,7 @@ export async function generateSystemPrompt() {
|
|
|
11
11
|
const rulesPath = path.resolve(__dirname, './templates/RULES.md');
|
|
12
12
|
rulesContent = await fs.readFile(rulesPath, 'utf-8');
|
|
13
13
|
}
|
|
14
|
-
catch (
|
|
14
|
+
catch (_error) {
|
|
15
15
|
console.warn('Could not read RULES.md, using default rules.');
|
|
16
16
|
rulesContent = 'Follow conventional commits.';
|
|
17
17
|
}
|
package/dist/providers/gemini.js
CHANGED
|
@@ -1,30 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { GoogleGenAI } from '@google/genai';
|
|
2
2
|
import { getModelList } from './models.js';
|
|
3
3
|
/**
|
|
4
4
|
* Check if error is a rate limit error (429)
|
|
5
5
|
*/
|
|
6
6
|
function isRateLimitError(error) {
|
|
7
7
|
const message = error?.message || '';
|
|
8
|
-
return message.includes('429') || message.includes('Too Many Requests') || message.includes('quota');
|
|
8
|
+
return (message.includes('429') || message.includes('Too Many Requests') || message.includes('quota'));
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* Generate content with a specific model
|
|
11
|
+
* Generate content with a specific model using the new Google GenAI SDK
|
|
12
12
|
*/
|
|
13
|
-
async function tryGenerateWithModel(
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
async function tryGenerateWithModel(ai, modelName, systemPrompt, userMessage) {
|
|
14
|
+
const response = await ai.models.generateContent({
|
|
15
|
+
model: modelName,
|
|
16
|
+
contents: userMessage,
|
|
17
|
+
config: {
|
|
18
|
+
systemInstruction: systemPrompt,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
return response.text ?? '';
|
|
21
22
|
}
|
|
22
23
|
/**
|
|
23
24
|
* Generates a commit suggestion using the Google Generative AI SDK (Gemini)
|
|
24
25
|
* Automatically falls back to other models when rate limit is hit
|
|
25
26
|
*/
|
|
26
27
|
export async function generateCommitSuggestion(apiKey, systemPrompt, diff, status, log, context = '') {
|
|
27
|
-
const
|
|
28
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
28
29
|
// Parse status to find untracked files
|
|
29
30
|
let untrackedMsg = '';
|
|
30
31
|
try {
|
|
@@ -38,16 +39,18 @@ IMPORTANT: The above files are NEW and untracked. You MUST suggest 'git add' for
|
|
|
38
39
|
`;
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
|
-
catch (
|
|
42
|
+
catch (_e) {
|
|
42
43
|
// metadata parsing failed, just ignore
|
|
43
44
|
}
|
|
44
45
|
// Build context section if provided
|
|
45
|
-
const contextSection = context
|
|
46
|
+
const contextSection = context
|
|
47
|
+
? `
|
|
46
48
|
USER CONTEXT (IMPORTANT - This describes the overall purpose of these changes):
|
|
47
49
|
"${context}"
|
|
48
50
|
|
|
49
51
|
Use this context to determine the appropriate commit type (feat, fix, refactor, chore, etc.) and to write more accurate commit messages.
|
|
50
|
-
`
|
|
52
|
+
`
|
|
53
|
+
: '';
|
|
51
54
|
const userMessage = `
|
|
52
55
|
${contextSection}
|
|
53
56
|
Git Status:
|
|
@@ -69,7 +72,7 @@ Please suggest the git add command and the git commit message.
|
|
|
69
72
|
// Try each model in order, fallback on rate limit
|
|
70
73
|
for (const modelName of modelsToTry) {
|
|
71
74
|
try {
|
|
72
|
-
const result = await tryGenerateWithModel(
|
|
75
|
+
const result = await tryGenerateWithModel(ai, modelName, systemPrompt, userMessage);
|
|
73
76
|
return result;
|
|
74
77
|
}
|
|
75
78
|
catch (error) {
|
package/dist/providers/models.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const GEMINI_MODELS = {
|
|
5
5
|
'flash-lite': 'gemini-2.5-flash-lite',
|
|
6
|
-
|
|
6
|
+
flash: 'gemini-2.5-flash',
|
|
7
7
|
'flash-3': 'gemini-3-flash',
|
|
8
8
|
};
|
|
9
9
|
/**
|
|
@@ -32,8 +32,5 @@ export function getModelList() {
|
|
|
32
32
|
const defaultModelName = GEMINI_MODELS[DEFAULT_MODEL];
|
|
33
33
|
const allModels = Object.values(GEMINI_MODELS);
|
|
34
34
|
// Put default model first, then the rest
|
|
35
|
-
return [
|
|
36
|
-
defaultModelName,
|
|
37
|
-
...allModels.filter(m => m !== defaultModelName)
|
|
38
|
-
];
|
|
35
|
+
return [defaultModelName, ...allModels.filter((m) => m !== defaultModelName)];
|
|
39
36
|
}
|
package/dist/utils/input.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xoegit",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "AI-powered CLI tool for generating semantic git commit messages and PR descriptions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ujangdoubleday",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"LICENSE.md"
|
|
24
24
|
],
|
|
25
25
|
"engines": {
|
|
26
|
-
"node": ">=
|
|
26
|
+
"node": ">=20.19.5"
|
|
27
27
|
},
|
|
28
28
|
"keywords": [
|
|
29
29
|
"git",
|
|
@@ -37,23 +37,48 @@
|
|
|
37
37
|
"productivity"
|
|
38
38
|
],
|
|
39
39
|
"scripts": {
|
|
40
|
-
"build": "tsc && mkdir -p dist/prompts/templates && cp src/prompts/templates/RULES.md dist/prompts/templates/RULES.md",
|
|
40
|
+
"build": "tsc -p tsconfig.build.json && mkdir -p dist/prompts/templates && cp src/prompts/templates/RULES.md dist/prompts/templates/RULES.md",
|
|
41
41
|
"start": "node dist/index.js",
|
|
42
42
|
"test": "vitest run",
|
|
43
43
|
"test:watch": "vitest",
|
|
44
|
+
"lint": "eslint src/ tests/",
|
|
45
|
+
"lint:fix": "eslint src/ tests/ --fix",
|
|
46
|
+
"format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'",
|
|
47
|
+
"format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts'",
|
|
48
|
+
"prepare": "husky",
|
|
44
49
|
"prepublishOnly": "npm run build && npm test"
|
|
45
50
|
},
|
|
46
51
|
"dependencies": {
|
|
47
|
-
"@google/
|
|
52
|
+
"@google/genai": "^1.34.0",
|
|
48
53
|
"chalk": "^5.6.2",
|
|
49
54
|
"commander": "^14.0.2",
|
|
50
55
|
"ora": "^8.1.0",
|
|
51
56
|
"simple-git": "^3.30.0"
|
|
52
57
|
},
|
|
53
58
|
"devDependencies": {
|
|
59
|
+
"@eslint/js": "^9.39.2",
|
|
54
60
|
"@types/node": "^25.0.3",
|
|
61
|
+
"eslint": "^9.39.2",
|
|
62
|
+
"eslint-config-prettier": "^10.1.8",
|
|
63
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
64
|
+
"husky": "^9.1.7",
|
|
65
|
+
"lint-staged": "^16.2.7",
|
|
66
|
+
"prettier": "^3.7.4",
|
|
55
67
|
"ts-node": "^10.9.2",
|
|
56
68
|
"typescript": "^5.7.0",
|
|
69
|
+
"typescript-eslint": "^8.51.0",
|
|
57
70
|
"vitest": "^4.0.16"
|
|
71
|
+
},
|
|
72
|
+
"lint-staged": {
|
|
73
|
+
"*.{js,jsx,ts,tsx}": [
|
|
74
|
+
"prettier --write",
|
|
75
|
+
"eslint --fix --max-warnings 0 --no-warn-ignored"
|
|
76
|
+
],
|
|
77
|
+
"eslint.config.js": [
|
|
78
|
+
"prettier --write"
|
|
79
|
+
],
|
|
80
|
+
"*.{json,md}": [
|
|
81
|
+
"prettier --write"
|
|
82
|
+
]
|
|
58
83
|
}
|
|
59
84
|
}
|