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 CHANGED
@@ -1,9 +1,10 @@
1
1
  # xoegit
2
2
 
3
- ![Node.js](https://img.shields.io/badge/Node.js-18+-339933?logo=node.js&logoColor=white)
4
- ![TypeScript](https://img.shields.io/badge/TypeScript-5.7-3178C6?logo=typescript&logoColor=white)
5
- ![Gemini](https://img.shields.io/badge/Gemini-AI-8E75B2?logo=google&logoColor=white)
6
- ![License](https://img.shields.io/badge/License-MIT-green)
3
+ [![Node.js](https://img.shields.io/badge/Node.js-20.19.5+-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
4
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
5
+ [![Gemini](https://img.shields.io/badge/Gemini-AI-8E75B2?logo=google&logoColor=white)](https://ai.google.dev/)
6
+ [![npm](https://img.shields.io/npm/v/xoegit?color=cb3837&logo=npm&logoColor=white)](https://www.npmjs.com/package/xoegit)
7
+ [![License](https://img.shields.io/badge/License-MIT-green)](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 18 or higher
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
+ ![xoegit](docs/xoegit-screenshoot.png)
56
+
54
57
  ### Options
55
58
 
56
59
  | Option | Description |
@@ -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 (err) {
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 (e) {
58
+ catch (_e) {
63
59
  // failed to parse
64
60
  }
65
61
  if (!hasDiff && !hasUntracked) {
@@ -20,7 +20,7 @@ export class ConfigService {
20
20
  return config.XOEGIT_GEMINI_API_KEY;
21
21
  }
22
22
  }
23
- catch (error) {
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 (e) {
37
+ catch (_e) {
38
38
  // ignore
39
39
  }
40
40
  config.XOEGIT_GEMINI_API_KEY = apiKey;
@@ -7,7 +7,7 @@ export async function isGitRepository() {
7
7
  try {
8
8
  return await git.checkIsRepo();
9
9
  }
10
- catch (error) {
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 (error) {
38
- return "No commits yet.";
37
+ catch (_error) {
38
+ return 'No commits yet.';
39
39
  }
40
40
  }
@@ -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 (error) {
14
+ catch (_error) {
15
15
  console.warn('Could not read RULES.md, using default rules.');
16
16
  rulesContent = 'Follow conventional commits.';
17
17
  }
@@ -1,30 +1,31 @@
1
- import { GoogleGenerativeAI } from '@google/generative-ai';
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(genAI, modelName, systemPrompt, userMessage) {
14
- const model = genAI.getGenerativeModel({ model: modelName });
15
- const result = await model.generateContent([
16
- { text: systemPrompt },
17
- { text: userMessage }
18
- ]);
19
- const response = await result.response;
20
- return response.text();
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 genAI = new GoogleGenerativeAI(apiKey);
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 (e) {
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(genAI, modelName, systemPrompt, userMessage);
75
+ const result = await tryGenerateWithModel(ai, modelName, systemPrompt, userMessage);
73
76
  return result;
74
77
  }
75
78
  catch (error) {
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const GEMINI_MODELS = {
5
5
  'flash-lite': 'gemini-2.5-flash-lite',
6
- 'flash': 'gemini-2.5-flash',
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
  }
@@ -56,7 +56,7 @@ export async function promptApiKey() {
56
56
  const rl = readline.createInterface({
57
57
  input: process.stdin,
58
58
  output: process.stdout,
59
- terminal: false
59
+ terminal: false,
60
60
  });
61
61
  rl.question('', (answer) => {
62
62
  rl.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xoegit",
3
- "version": "1.0.1",
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": ">=18.0.0"
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/generative-ai": "^0.24.1",
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
  }