gitpal-cli 1.0.2 → 1.0.4

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,11 +1,11 @@
1
1
  # 🤖 GitPal — AI-Powered Git Assistant CLI
2
- <!--
2
+
3
3
  > Stop writing commit messages manually. Let AI do it in 3 seconds.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/gitpal-cli.svg?style=flat-square)](https://www.npmjs.com/package/gitpal-cli)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/gitpal-cli.svg?style=flat-square)](https://www.npmjs.com/package/gitpal-cli)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
8
- [![Node.js](https://img.shields.io/badge/Node.js-18%2B-green?style=flat-square)](https://nodejs.org) -->
8
+ [![Node.js](https://img.shields.io/badge/Node.js-18%2B-green?style=flat-square)](https://nodejs.org)
9
9
 
10
10
  ---
11
11
 
@@ -193,6 +193,36 @@ gitpal config
193
193
 
194
194
  ---
195
195
 
196
+ ### `gitpal review` — AI Code Reviewer
197
+ Reviews your staged code for bugs, security issues and bad practices before you commit — like having a senior developer on your team 24/7.
198
+
199
+ ```bash
200
+ git add .
201
+ gitpal review
202
+
203
+ # 🐛 Bugs Found:
204
+ # - No error handling on login failure
205
+ #
206
+ # 🔒 Security Issues:
207
+ # - Password stored as plain text, use bcrypt
208
+ #
209
+ # 💡 Improvements:
210
+ # - Add input validation for username and password
211
+ #
212
+ # ❌ Verdict: Do not commit
213
+
214
+ # ? What would you like to do?
215
+ # ✅ Looks good — generate commit message and commit
216
+ # ❌ I will fix the issues first
217
+ ```
218
+
219
+ **Options:**
220
+ ```bash
221
+ gitpal review --review-only # Only review, skip commit step
222
+ ```
223
+
224
+ ---
225
+
196
226
  ## 🔄 Full Daily Workflow
197
227
 
198
228
  ```
@@ -201,6 +231,8 @@ Morning — open your project
201
231
  Write some code (auth feature)
202
232
 
203
233
  git add .
234
+ gitpal review → AI checks for bugs and security issues
235
+
204
236
  gitpal commit → "feat(auth): add Google OAuth login"
205
237
 
206
238
  Write more code (fix a bug)
@@ -228,6 +260,7 @@ gitpal changelog --ver 2.0.0 → Full changelog ready
228
260
  | Spend 15 mins on PR description | Generated in 3 seconds |
229
261
  | Forget what you built last week | Plain English summary instantly |
230
262
  | Write changelog manually | Auto-generated from commits |
263
+ | No code review before commit | AI catches bugs before they reach GitHub |
231
264
  | Works with one AI only | Works with 4 AI providers |
232
265
 
233
266
  ---
@@ -247,7 +280,8 @@ gitpal/
247
280
  │ ├── summary.js ← gitpal summary
248
281
  │ ├── pr.js ← gitpal pr
249
282
  │ ├── changelog.js ← gitpal changelog
250
- └── config.js ← gitpal config
283
+ ├── config.js ← gitpal config
284
+ │ └── review.js ← gitpal review
251
285
  └── tests/
252
286
  └── ai.test.js
253
287
  ```
@@ -258,7 +292,7 @@ gitpal/
258
292
 
259
293
  ```bash
260
294
  # Clone the repo
261
- git clone https://github.com/harshit_gangwar16/gitpal
295
+ git clone https://github.com/h1a2r3s4h/gitpal
262
296
  cd gitpal
263
297
 
264
298
  # Install dependencies
@@ -292,7 +326,7 @@ Contributions are welcome! To add a new AI provider:
292
326
 
293
327
  Built by **Harshit Gangwar**
294
328
 
295
- - GitHub: [@harshit_gangwar16](https://github.com/harshit_gangwar16)
329
+ - GitHub: [@h1a2r3s4h](https://github.com/h1a2r3s4h)
296
330
  - npm: [gitpal-cli](https://www.npmjs.com/package/gitpal-cli)
297
331
 
298
332
  ---
@@ -305,4 +339,4 @@ MIT — free to use, modify and distribute.
305
339
 
306
340
  <p align="center">
307
341
  <strong>If GitPal saves you time, give it a ⭐ on GitHub!</strong>
308
- </p>
342
+ </p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitpal-cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "AI-powered Git assistant CLI",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -26,5 +26,8 @@
26
26
  "ora": "^7.0.1",
27
27
  "simple-git": "^3.21.0"
28
28
  },
29
- "type": "module"
29
+ "type": "module",
30
+ "devDependencies": {
31
+ "jest": "^30.3.0"
32
+ }
30
33
  }
@@ -0,0 +1,181 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { isGitRepo } from '../git.js';
6
+ import { askAI } from '../ai.js';
7
+ import simpleGit from 'simple-git';
8
+
9
+ const git = simpleGit();
10
+
11
+ export async function explainCommand(target, options) {
12
+ // 1. Guard: must be inside a git repo
13
+ if (!(await isGitRepo())) {
14
+ console.log(chalk.red('❌ Not a git repository.'));
15
+ process.exit(1);
16
+ }
17
+
18
+ // 2. Decide what to explain
19
+ if (!target) {
20
+ console.log(chalk.red('❌ Please provide something to explain.'));
21
+ console.log(chalk.dim('\nExamples:'));
22
+ console.log(chalk.cyan(' gitpal explain a3f2c1 ') + chalk.dim('← explain a commit'));
23
+ console.log(chalk.cyan(' gitpal explain src/auth.js ') + chalk.dim('← explain a file'));
24
+ console.log(chalk.cyan(' gitpal explain src/auth.js --function login') + chalk.dim('← explain a function'));
25
+ process.exit(1);
26
+ }
27
+
28
+ // 3. Check if target is a file or a commit hash
29
+ const isFile = fs.existsSync(target);
30
+ const isCommitHash = /^[0-9a-f]{6,40}$/i.test(target);
31
+
32
+ if (isFile) {
33
+ await explainFile(target, options);
34
+ } else if (isCommitHash) {
35
+ await explainCommit(target);
36
+ } else {
37
+ console.log(chalk.red(`❌ "${target}" is not a valid file or commit hash.`));
38
+ process.exit(1);
39
+ }
40
+ }
41
+
42
+ // ─── EXPLAIN FILE ─────────────────────────────────────────────────────────────
43
+
44
+ async function explainFile(filePath, options) {
45
+ const spinner = ora(`Reading ${filePath}...`).start();
46
+
47
+ let code;
48
+ try {
49
+ code = fs.readFileSync(filePath, 'utf-8');
50
+ } catch {
51
+ spinner.fail(chalk.red(`Cannot read file: ${filePath}`));
52
+ process.exit(1);
53
+ }
54
+
55
+ if (!code.trim()) {
56
+ spinner.fail(chalk.yellow('File is empty.'));
57
+ process.exit(1);
58
+ }
59
+
60
+ spinner.succeed(`Read ${path.basename(filePath)} successfully.`);
61
+
62
+ const aiSpinner = ora('AI is analyzing the code...').start();
63
+
64
+ let prompt;
65
+
66
+ if (options.function) {
67
+ // Explain a specific function
68
+ prompt = `You are a senior developer explaining code to a junior developer.
69
+
70
+ Analyze this code and explain ONLY the function named "${options.function}".
71
+
72
+ Explain:
73
+ 1. What this function does in simple words
74
+ 2. What inputs it takes
75
+ 3. What it returns
76
+ 4. Step by step what happens inside it
77
+ 5. Any important things to know
78
+
79
+ Use simple language. No jargon. Maximum 10 lines.
80
+ End with: "Depends on: X, Y, Z" (list any libraries or functions it uses)
81
+
82
+ File: ${filePath}
83
+ Code:
84
+ ${code.slice(0, 4000)}`;
85
+ } else {
86
+ // Explain the entire file
87
+ prompt = `You are a senior developer explaining code to a junior developer.
88
+
89
+ Analyze this entire file and explain it clearly.
90
+
91
+ Tell me:
92
+ 1. What is the PURPOSE of this file in one sentence
93
+ 2. What are the MAIN functions/classes (list each with one line description)
94
+ 3. How does it FIT into a typical project
95
+ 4. Any IMPORTANT patterns or techniques used
96
+
97
+ Use simple language. Be concise. Maximum 15 lines.
98
+ End with: "Depends on: X, Y, Z" (list key imports/dependencies)
99
+
100
+ File: ${filePath}
101
+ Code:
102
+ ${code.slice(0, 4000)}`;
103
+ }
104
+
105
+ try {
106
+ const explanation = await askAI(prompt);
107
+ aiSpinner.succeed('Explanation ready!\n');
108
+
109
+ const title = options.function
110
+ ? `📖 Explaining function: ${chalk.cyan(options.function)}() in ${chalk.dim(filePath)}`
111
+ : `📖 Explaining file: ${chalk.cyan(path.basename(filePath))}`;
112
+
113
+ console.log(chalk.bold(title));
114
+ console.log(chalk.dim('─'.repeat(50)));
115
+ console.log(chalk.white(explanation));
116
+ console.log('');
117
+
118
+ } catch (err) {
119
+ aiSpinner.fail(chalk.red(`AI Error: ${err.message}`));
120
+ process.exit(1);
121
+ }
122
+ }
123
+
124
+ // ─── EXPLAIN COMMIT ───────────────────────────────────────────────────────────
125
+
126
+ async function explainCommit(hash) {
127
+ const spinner = ora(`Fetching commit ${hash}...`).start();
128
+
129
+ let diff, log;
130
+ try {
131
+ diff = await git.show([hash, '--stat', '--patch']);
132
+ log = await git.log({ from: `${hash}^`, to: hash, maxCount: 1 });
133
+ } catch {
134
+ spinner.fail(chalk.red(`Commit "${hash}" not found.`));
135
+ process.exit(1);
136
+ }
137
+
138
+ spinner.succeed('Commit found.');
139
+
140
+ const aiSpinner = ora('AI is analyzing the commit...').start();
141
+
142
+ const commitMessage = log.all[0]?.message || 'No message';
143
+ const commitDate = log.all[0]?.date || '';
144
+ const commitAuthor = log.all[0]?.author_name || '';
145
+
146
+ const prompt = `You are a senior developer explaining a git commit to a junior developer.
147
+
148
+ Explain this commit in plain English.
149
+
150
+ Tell me:
151
+ 1. WHAT changed — what was added, removed or modified
152
+ 2. WHY it was likely changed — what problem it solves
153
+ 3. FILES affected — list each file and what changed in it
154
+ 4. IMPACT — how does this affect the overall project
155
+
156
+ Use simple language. Be specific. Maximum 15 lines.
157
+
158
+ Commit: ${hash}
159
+ Message: ${commitMessage}
160
+ Author: ${commitAuthor}
161
+ Date: ${commitDate}
162
+
163
+ Diff:
164
+ ${diff.slice(0, 4000)}`;
165
+
166
+ try {
167
+ const explanation = await askAI(prompt);
168
+ aiSpinner.succeed('Explanation ready!\n');
169
+
170
+ console.log(chalk.bold(`📖 Explaining commit: ${chalk.cyan(hash)}`));
171
+ console.log(chalk.dim(`Message: ${commitMessage}`));
172
+ console.log(chalk.dim(`Author: ${commitAuthor} | Date: ${commitDate}`));
173
+ console.log(chalk.dim('─'.repeat(50)));
174
+ console.log(chalk.white(explanation));
175
+ console.log('');
176
+
177
+ } catch (err) {
178
+ aiSpinner.fail(chalk.red(`AI Error: ${err.message}`));
179
+ process.exit(1);
180
+ }
181
+ }
package/src/index.js CHANGED
@@ -6,7 +6,7 @@ import { prCommand } from './commands/pr.js';
6
6
  import { changelogCommand } from './commands/changelog.js';
7
7
  import { configCommand } from './commands/config.js';
8
8
  import { reviewCommand } from './commands/review.js';
9
-
9
+ import { explainCommand } from './commands/explain.js';
10
10
  const program = new Command();
11
11
 
12
12
  console.log(chalk.cyan.bold('\n🤖 GitPal — Your AI Git Assistant\n'));
@@ -53,7 +53,11 @@ program
53
53
  .option('-r, --review-only', 'Only review, do not commit')
54
54
  .action(reviewCommand);
55
55
 
56
-
56
+ program
57
+ .command('explain <target>')
58
+ .description('Explain any file or commit in plain English')
59
+ .option('-f, --function <name>', 'Explain a specific function')
60
+ .action(explainCommand);
57
61
  program.parse(process.argv);
58
62
 
59
63
  // Show help if no command given
package/tests/ai.test.js CHANGED
@@ -0,0 +1,207 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+
6
+ const CONFIG_PATH = path.join(os.homedir(), '.gitpal.json');
7
+
8
+ function cleanConfig() {
9
+ if (fs.existsSync(CONFIG_PATH)) {
10
+ fs.unlinkSync(CONFIG_PATH);
11
+ }
12
+ }
13
+
14
+ // ─── CONFIG TESTS ─────────────────────────────────────────────────────────────
15
+
16
+ describe('Config Management', () => {
17
+
18
+ beforeEach(() => cleanConfig());
19
+ afterEach(() => cleanConfig());
20
+
21
+ test('loadConfig returns empty object when no config exists', async () => {
22
+ const { loadConfig } = await import('../src/ai.js');
23
+ const config = loadConfig();
24
+ expect(config).toEqual({});
25
+ });
26
+
27
+ test('saveConfig saves provider and apiKey correctly', async () => {
28
+ const { saveConfig, loadConfig } = await import('../src/ai.js');
29
+ saveConfig({ provider: 'groq', apiKey: 'test-key-123' });
30
+ const config = loadConfig();
31
+ expect(config.provider).toBe('groq');
32
+ expect(config.apiKey).toBe('test-key-123');
33
+ });
34
+
35
+ test('saveConfig overwrites existing config', async () => {
36
+ const { saveConfig, loadConfig } = await import('../src/ai.js');
37
+ saveConfig({ provider: 'openai', apiKey: 'old-key' });
38
+ saveConfig({ provider: 'anthropic', apiKey: 'new-key' });
39
+ const config = loadConfig();
40
+ expect(config.provider).toBe('anthropic');
41
+ expect(config.apiKey).toBe('new-key');
42
+ });
43
+
44
+ test('saveConfig supports all 4 providers', async () => {
45
+ const { saveConfig, loadConfig } = await import('../src/ai.js');
46
+ const providers = ['groq', 'openai', 'gemini', 'anthropic'];
47
+ for (const provider of providers) {
48
+ saveConfig({ provider, apiKey: `key-for-${provider}` });
49
+ const config = loadConfig();
50
+ expect(config.provider).toBe(provider);
51
+ }
52
+ });
53
+
54
+ test('config file is created at correct path', async () => {
55
+ const { saveConfig } = await import('../src/ai.js');
56
+ saveConfig({ provider: 'groq', apiKey: 'test-key' });
57
+ expect(fs.existsSync(CONFIG_PATH)).toBe(true);
58
+ });
59
+
60
+ test('config file contains valid JSON', async () => {
61
+ const { saveConfig } = await import('../src/ai.js');
62
+ saveConfig({ provider: 'groq', apiKey: 'test-key' });
63
+ const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
64
+ expect(() => JSON.parse(raw)).not.toThrow();
65
+ });
66
+
67
+ });
68
+
69
+ // ─── AI PROVIDER TESTS ────────────────────────────────────────────────────────
70
+
71
+ describe('AI Provider Validation', () => {
72
+
73
+ beforeEach(() => cleanConfig());
74
+ afterEach(() => cleanConfig());
75
+
76
+ test('askAI throws error when no config exists', async () => {
77
+ const { askAI } = await import('../src/ai.js');
78
+ await expect(askAI('test prompt')).rejects.toThrow('No AI provider configured');
79
+ });
80
+
81
+ test('askAI throws error for unknown provider', async () => {
82
+ const { saveConfig, askAI } = await import('../src/ai.js');
83
+ saveConfig({ provider: 'unknownprovider', apiKey: 'test-key' });
84
+ await expect(askAI('test prompt')).rejects.toThrow();
85
+ });
86
+
87
+ test('askAI throws error when apiKey is missing', async () => {
88
+ const { saveConfig, askAI } = await import('../src/ai.js');
89
+ saveConfig({ provider: 'groq', apiKey: '' });
90
+ await expect(askAI('test prompt')).rejects.toThrow();
91
+ });
92
+
93
+ });
94
+
95
+ // ─── GIT UTILITY TESTS ────────────────────────────────────────────────────────
96
+
97
+ describe('Git Utilities', () => {
98
+
99
+ test('isGitRepo returns a boolean', async () => {
100
+ const { isGitRepo } = await import('../src/git.js');
101
+ const result = await isGitRepo();
102
+ expect(typeof result).toBe('boolean');
103
+ });
104
+
105
+ test('isGitRepo returns true inside a git repo', async () => {
106
+ const { isGitRepo } = await import('../src/git.js');
107
+ const result = await isGitRepo();
108
+ expect(result).toBe(true);
109
+ });
110
+
111
+ test('getRecentCommits returns an array', async () => {
112
+ const { getRecentCommits } = await import('../src/git.js');
113
+ const commits = await getRecentCommits(3);
114
+ expect(Array.isArray(commits)).toBe(true);
115
+ });
116
+
117
+ test('getRecentCommits respects the limit', async () => {
118
+ const { getRecentCommits } = await import('../src/git.js');
119
+ const commits = await getRecentCommits(2);
120
+ expect(commits.length).toBeLessThanOrEqual(2);
121
+ });
122
+
123
+ test('getCurrentBranch returns a string', async () => {
124
+ const { getCurrentBranch } = await import('../src/git.js');
125
+ const branch = await getCurrentBranch();
126
+ expect(typeof branch).toBe('string');
127
+ expect(branch.length).toBeGreaterThan(0);
128
+ });
129
+
130
+ test('getStagedDiff returns a string', async () => {
131
+ const { getStagedDiff } = await import('../src/git.js');
132
+ const diff = await getStagedDiff();
133
+ expect(typeof diff).toBe('string');
134
+ });
135
+
136
+ });
137
+
138
+ // ─── PACKAGE.JSON VALIDATION ──────────────────────────────────────────────────
139
+
140
+ describe('Package Configuration', () => {
141
+
142
+ test('package.json has correct name', () => {
143
+ const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf-8'));
144
+ expect(pkg.name).toBe('gitpal-cli');
145
+ });
146
+
147
+ test('package.json has bin field', () => {
148
+ const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf-8'));
149
+ expect(pkg.bin).toBeDefined();
150
+ });
151
+
152
+ test('package.json has version', () => {
153
+ const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf-8'));
154
+ expect(pkg.version).toBeDefined();
155
+ expect(pkg.version).toMatch(/^\d+\.\d+\.\d+$/);
156
+ });
157
+
158
+ test('package.json has required dependencies', () => {
159
+ const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf-8'));
160
+ const required = ['commander', 'simple-git', 'chalk', 'ora', 'inquirer'];
161
+ required.forEach(dep => {
162
+ expect(pkg.dependencies).toHaveProperty(dep);
163
+ });
164
+ });
165
+
166
+ test('bin/gitpal.js file exists and is not empty', () => {
167
+ const binPath = './bin/gitpal.js';
168
+ expect(fs.existsSync(binPath)).toBe(true);
169
+ const content = fs.readFileSync(binPath, 'utf-8');
170
+ expect(content.trim().length).toBeGreaterThan(0);
171
+ });
172
+
173
+ });
174
+
175
+ // ─── COMMAND FILES EXIST ──────────────────────────────────────────────────────
176
+
177
+ describe('Command Files', () => {
178
+
179
+ const commands = ['commit', 'summary', 'pr', 'changelog', 'config', 'review'];
180
+
181
+ commands.forEach(cmd => {
182
+ test(`src/commands/${cmd}.js exists`, () => {
183
+ expect(fs.existsSync(`./src/commands/${cmd}.js`)).toBe(true);
184
+ });
185
+
186
+ test(`src/commands/${cmd}.js is not empty`, () => {
187
+ const content = fs.readFileSync(`./src/commands/${cmd}.js`, 'utf-8');
188
+ expect(content.trim().length).toBeGreaterThan(0);
189
+ });
190
+ });
191
+
192
+ test('src/ai.js exists and is not empty', () => {
193
+ const content = fs.readFileSync('./src/ai.js', 'utf-8');
194
+ expect(content.trim().length).toBeGreaterThan(0);
195
+ });
196
+
197
+ test('src/git.js exists and is not empty', () => {
198
+ const content = fs.readFileSync('./src/git.js', 'utf-8');
199
+ expect(content.trim().length).toBeGreaterThan(0);
200
+ });
201
+
202
+ test('src/index.js exists and is not empty', () => {
203
+ const content = fs.readFileSync('./src/index.js', 'utf-8');
204
+ expect(content.trim().length).toBeGreaterThan(0);
205
+ });
206
+
207
+ });