gitpal-harshit 1.0.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 ADDED
File without changes
package/bin/gitpal.js ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "gitpal-harshit",
3
+ "version": "1.0.0",
4
+ "description": "AI-powered Git assistant CLI",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "gitpal": "./bin/gitpal.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node --experimental-vm-modules node_modules/.bin/jest"
11
+ },
12
+ "keywords": [
13
+ "git",
14
+ "cli",
15
+ "ai",
16
+ "automation"
17
+ ],
18
+ "author": "",
19
+ "license": "ISC",
20
+ "dependencies": {
21
+ "chalk": "^5.3.0",
22
+ "commander": "^11.1.0",
23
+ "dotenv": "^16.3.1",
24
+ "inquirer": "^9.2.12",
25
+ "node-fetch": "^3.3.2",
26
+ "ora": "^7.0.1",
27
+ "simple-git": "^3.21.0"
28
+ },
29
+ "type": "module"
30
+ }
package/src/ai.js ADDED
@@ -0,0 +1,110 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ // Config file stored in user's home directory
6
+ const CONFIG_PATH = path.join(os.homedir(), '.gitpal.json');
7
+
8
+ export function loadConfig() {
9
+ if (!fs.existsSync(CONFIG_PATH)) return {};
10
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
11
+ }
12
+
13
+ export function saveConfig(config) {
14
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
15
+ }
16
+
17
+ // ─── AI PROVIDER ROUTER ───────────────────────────────────────────────────────
18
+ // All providers receive the same prompt and return a plain string response.
19
+ // Adding a new provider = add one function + one case below.
20
+
21
+ async function callAnthropic(prompt, apiKey) {
22
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
23
+ method: 'POST',
24
+ headers: {
25
+ 'Content-Type': 'application/json',
26
+ 'x-api-key': apiKey,
27
+ 'anthropic-version': '2023-06-01',
28
+ },
29
+ body: JSON.stringify({
30
+ model: 'claude-3-haiku-20240307',
31
+ max_tokens: 500,
32
+ messages: [{ role: 'user', content: prompt }],
33
+ }),
34
+ });
35
+ const data = await res.json();
36
+ if (!res.ok) throw new Error(data.error?.message || 'Anthropic API error');
37
+ return data.content[0].text.trim();
38
+ }
39
+
40
+ async function callOpenAI(prompt, apiKey) {
41
+ const res = await fetch('https://api.openai.com/v1/chat/completions', {
42
+ method: 'POST',
43
+ headers: {
44
+ 'Content-Type': 'application/json',
45
+ Authorization: `Bearer ${apiKey}`,
46
+ },
47
+ body: JSON.stringify({
48
+ model: 'gpt-3.5-turbo',
49
+ max_tokens: 500,
50
+ messages: [{ role: 'user', content: prompt }],
51
+ }),
52
+ });
53
+ const data = await res.json();
54
+ if (!res.ok) throw new Error(data.error?.message || 'OpenAI API error');
55
+ return data.choices[0].message.content.trim();
56
+ }
57
+
58
+ async function callGemini(prompt, apiKey) {
59
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${apiKey}`;
60
+ const res = await fetch(url, {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({
64
+ contents: [{ parts: [{ text: prompt }] }],
65
+ }),
66
+ });
67
+ const data = await res.json();
68
+ if (!res.ok) throw new Error(data.error?.message || 'Gemini API error');
69
+ return data.candidates[0].content.parts[0].text.trim();
70
+ }
71
+
72
+ async function callGroq(prompt, apiKey) {
73
+ const res = await fetch('https://api.groq.com/openai/v1/chat/completions', {
74
+ method: 'POST',
75
+ headers: {
76
+ 'Content-Type': 'application/json',
77
+ Authorization: `Bearer ${apiKey}`,
78
+ },
79
+ body: JSON.stringify({
80
+ model: 'llama-3.3-70b-versatile',
81
+ max_tokens: 500,
82
+ messages: [{ role: 'user', content: prompt }],
83
+ }),
84
+ });
85
+ const data = await res.json();
86
+ if (!res.ok) throw new Error(data.error?.message || 'Groq API error');
87
+ return data.choices[0].message.content.trim();
88
+ }
89
+
90
+ // ─── MAIN EXPORT ─────────────────────────────────────────────────────────────
91
+
92
+ export async function askAI(prompt) {
93
+ const config = loadConfig();
94
+
95
+ const provider = config.provider;
96
+ const apiKey = config.apiKey;
97
+
98
+ if (!provider || !apiKey) {
99
+ throw new Error('No AI provider configured. Run: gitpal config');
100
+ }
101
+
102
+ switch (provider) {
103
+ case 'anthropic': return callAnthropic(prompt, apiKey);
104
+ case 'openai': return callOpenAI(prompt, apiKey);
105
+ case 'gemini': return callGemini(prompt, apiKey);
106
+ case 'groq': return callGroq(prompt, apiKey);
107
+ default:
108
+ throw new Error(`Unknown provider "${provider}". Run: gitpal config`);
109
+ }
110
+ }
@@ -0,0 +1,66 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import { getRecentCommits, isGitRepo } from "../git.js";
4
+ import { askAI } from "../ai.js";
5
+
6
+ export async function changelogCommand(options) {
7
+ if (!(await isGitRepo())) {
8
+ console.log(chalk.red("❌ Not a git repository."));
9
+ process.exit(1);
10
+ }
11
+
12
+ const version = options.ver || "1.0.0";
13
+ const n = options.last || 20;
14
+
15
+ const spinner = ora(`Fetching last ${n} commits for changelog...`).start();
16
+ const commits = await getRecentCommits(n);
17
+
18
+ if (!commits.length) {
19
+ spinner.fail("No commits found.");
20
+ process.exit(1);
21
+ }
22
+ spinner.succeed(`Found ${commits.length} commits.`);
23
+
24
+ const aiSpinner = ora("Generating changelog with AI...").start();
25
+
26
+ const date = new Date().toISOString().split("T")[0];
27
+
28
+ const prompt = `Generate a professional CHANGELOG entry from these git commits.
29
+
30
+ Format exactly like this:
31
+ ## [${version}] - ${date}
32
+
33
+ ### Features
34
+ - (new features added)
35
+
36
+ ### Bug Fixes
37
+ - (bugs fixed)
38
+
39
+ ### Improvements
40
+ - (improvements/refactors)
41
+
42
+ ### Documentation
43
+ - (doc changes if any)
44
+
45
+ Rules:
46
+ - Only include sections that have relevant commits
47
+ - Be concise and user-friendly
48
+ - Skip merge commits
49
+ - Group similar changes
50
+
51
+ Commits:
52
+ ${commits.join("\n")}`;
53
+
54
+ try {
55
+ const changelog = await askAI(prompt);
56
+ aiSpinner.succeed("Changelog ready!\n");
57
+
58
+ console.log(chalk.bold(`📄 CHANGELOG v${version}:\n`));
59
+ console.log(chalk.white(changelog));
60
+ console.log("");
61
+ console.log(chalk.dim("💡 Add this to your CHANGELOG.md file."));
62
+ } catch (err) {
63
+ aiSpinner.fail(chalk.red(`AI Error: ${err.message}`));
64
+ process.exit(1);
65
+ }
66
+ }
@@ -0,0 +1,91 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import inquirer from "inquirer";
4
+ import { getStagedDiff, doCommit, isGitRepo } from "../git.js";
5
+ import { askAI } from "../ai.js";
6
+
7
+ export async function commitCommand(options) {
8
+ // 1. Guard: must be inside a git repo
9
+ if (!(await isGitRepo())) {
10
+ console.log(chalk.red("❌ Not a git repository."));
11
+ process.exit(1);
12
+ }
13
+
14
+ // 2. Get staged diff
15
+ const spinner = ora("Reading your staged changes...").start();
16
+ const diff = await getStagedDiff();
17
+
18
+ if (!diff || diff.trim() === "") {
19
+ spinner.fail(chalk.yellow("No staged changes found. Run: git add <files>"));
20
+ process.exit(1);
21
+ }
22
+ spinner.succeed("Staged changes found.");
23
+
24
+ // 3. Ask AI to generate commit message
25
+ const aiSpinner = ora("Generating commit message with AI...").start();
26
+
27
+ const prompt = `You are an expert developer. Analyze this git diff and write a concise, conventional commit message.
28
+
29
+ Rules:
30
+ - Use conventional commits format: type(scope): description
31
+ - Types: feat, fix, docs, style, refactor, test, chore
32
+ - Keep it under 72 characters
33
+ - Be specific about what changed
34
+ - Return ONLY the commit message, nothing else
35
+
36
+ Git Diff:
37
+ ${diff.slice(0, 3000)}`; // Limit diff size to avoid token limits
38
+
39
+ let message;
40
+ try {
41
+ message = await askAI(prompt);
42
+ aiSpinner.succeed(chalk.green("Commit message generated!"));
43
+ } catch (err) {
44
+ aiSpinner.fail(chalk.red(`AI Error: ${err.message}`));
45
+ process.exit(1);
46
+ }
47
+
48
+ // 4. Show the message
49
+ console.log("\n" + chalk.bold("Suggested commit message:"));
50
+ console.log(chalk.cyan(` ${message}\n`));
51
+
52
+ // 5. Confirm or skip
53
+ if (options.yes) {
54
+ await doCommit(message);
55
+ console.log(chalk.green.bold("✅ Committed successfully!"));
56
+ return;
57
+ }
58
+
59
+ const { action } = await inquirer.prompt([
60
+ {
61
+ type: "list",
62
+ name: "action",
63
+ message: "What would you like to do?",
64
+ choices: [
65
+ { name: "✅ Use this message and commit", value: "commit" },
66
+ { name: "✏️ Edit the message", value: "edit" },
67
+ { name: "❌ Cancel", value: "cancel" },
68
+ ],
69
+ },
70
+ ]);
71
+
72
+ if (action === "cancel") {
73
+ console.log(chalk.yellow("Cancelled."));
74
+ return;
75
+ }
76
+
77
+ if (action === "edit") {
78
+ const { edited } = await inquirer.prompt([
79
+ {
80
+ type: "input",
81
+ name: "edited",
82
+ message: "Edit commit message:",
83
+ default: message,
84
+ },
85
+ ]);
86
+ message = edited;
87
+ }
88
+
89
+ await doCommit(message);
90
+ console.log(chalk.green.bold("\n✅ Committed successfully!"));
91
+ }
@@ -0,0 +1,81 @@
1
+ import chalk from "chalk";
2
+ import inquirer from "inquirer";
3
+ import { loadConfig, saveConfig } from "../ai.js";
4
+
5
+ const PROVIDERS = {
6
+ anthropic: {
7
+ label: "Anthropic (Claude) — claude-3-haiku",
8
+ keyHint: "Get free credits at console.anthropic.com",
9
+ },
10
+ openai: {
11
+ label: "OpenAI (GPT-3.5) — gpt-3.5-turbo",
12
+ keyHint: "Get API key at platform.openai.com",
13
+ },
14
+ gemini: {
15
+ label: "Google Gemini — gemini-pro (Free tier)",
16
+ keyHint: "Get free API key at aistudio.google.com",
17
+ },
18
+ groq: {
19
+ label: "Groq — llama3-8b (Free & Ultra Fast)",
20
+ keyHint: "Get free API key at console.groq.com",
21
+ },
22
+ };
23
+
24
+ export async function configCommand() {
25
+ const existing = loadConfig();
26
+
27
+ console.log(chalk.bold("\n⚙️ GitPal Configuration\n"));
28
+
29
+ if (existing.provider) {
30
+ console.log(chalk.dim(`Current provider: ${existing.provider}`));
31
+ console.log(
32
+ chalk.dim(`Current API key: ${existing.apiKey?.slice(0, 8)}...\n`),
33
+ );
34
+ }
35
+
36
+ const { provider } = await inquirer.prompt([
37
+ {
38
+ type: "list",
39
+ name: "provider",
40
+ message: "Choose your AI provider:",
41
+ choices: Object.entries(PROVIDERS).map(([value, { label }]) => ({
42
+ name: label,
43
+ value,
44
+ })),
45
+ default: existing.provider,
46
+ },
47
+ ]);
48
+
49
+ const hint = PROVIDERS[provider].keyHint;
50
+ console.log(chalk.dim(`\n💡 ${hint}\n`));
51
+
52
+ const { apiKey } = await inquirer.prompt([
53
+ {
54
+ type: "password",
55
+ name: "apiKey",
56
+ message: `Enter your ${provider} API key:`,
57
+ mask: "*",
58
+ validate: (val) => val.trim().length > 0 || "API key cannot be empty",
59
+ },
60
+ ]);
61
+
62
+ saveConfig({ provider, apiKey: apiKey.trim() });
63
+
64
+ console.log(chalk.green.bold("\n✅ Configuration saved!"));
65
+ console.log(chalk.dim("Config stored at: ~/.gitpal.json\n"));
66
+ console.log(chalk.white("You can now run:"));
67
+ console.log(
68
+ chalk.cyan(" gitpal commit ") + chalk.dim("— auto commit message"),
69
+ );
70
+ console.log(
71
+ chalk.cyan(" gitpal summary ") +
72
+ chalk.dim("— summarize recent commits"),
73
+ );
74
+ console.log(
75
+ chalk.cyan(" gitpal pr ") + chalk.dim("— generate PR description"),
76
+ );
77
+ console.log(
78
+ chalk.cyan(" gitpal changelog ") + chalk.dim("— generate changelog"),
79
+ );
80
+ console.log("");
81
+ }
@@ -0,0 +1,72 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import { getBranchDiff, getCurrentBranch, isGitRepo } from "../git.js";
4
+ import { askAI } from "../ai.js";
5
+
6
+ export async function prCommand(options) {
7
+ if (!(await isGitRepo())) {
8
+ console.log(chalk.red("❌ Not a git repository."));
9
+ process.exit(1);
10
+ }
11
+
12
+ const baseBranch = options.base || "main";
13
+ const currentBranch = await getCurrentBranch();
14
+
15
+ console.log(
16
+ chalk.dim(
17
+ `Comparing ${chalk.cyan(currentBranch)} → ${chalk.cyan(baseBranch)}\n`,
18
+ ),
19
+ );
20
+
21
+ const spinner = ora("Reading branch diff...").start();
22
+ let diff;
23
+ try {
24
+ diff = await getBranchDiff(baseBranch);
25
+ } catch {
26
+ spinner.fail(
27
+ `Could not diff against "${baseBranch}". Is the branch name correct?`,
28
+ );
29
+ process.exit(1);
30
+ }
31
+
32
+ if (!diff || diff.trim() === "") {
33
+ spinner.warn("No differences found between branches.");
34
+ process.exit(0);
35
+ }
36
+ spinner.succeed("Diff ready.");
37
+
38
+ const aiSpinner = ora("Generating PR description with AI...").start();
39
+
40
+ const prompt = `You are a senior developer. Write a professional GitHub Pull Request description based on this git diff.
41
+
42
+ Format it exactly like this:
43
+ ## What changed
44
+ (bullet points of main changes)
45
+
46
+ ## Why
47
+ (brief reason for the change)
48
+
49
+ ## Type of change
50
+ (Bug fix / New feature / Refactor / Documentation)
51
+
52
+ ## Testing
53
+ (what to test or how it was tested)
54
+
55
+ Git Diff:
56
+ ${diff.slice(0, 4000)}`;
57
+
58
+ try {
59
+ const prDesc = await askAI(prompt);
60
+ aiSpinner.succeed("PR description ready!\n");
61
+
62
+ console.log(chalk.bold("📝 Pull Request Description:\n"));
63
+ console.log(chalk.white(prDesc));
64
+ console.log("");
65
+ console.log(
66
+ chalk.dim("💡 Copy the above and paste into your GitHub PR description."),
67
+ );
68
+ } catch (err) {
69
+ aiSpinner.fail(chalk.red(`AI Error: ${err.message}`));
70
+ process.exit(1);
71
+ }
72
+ }
@@ -0,0 +1,46 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import { getRecentCommits, isGitRepo } from "../git.js";
4
+ import { askAI } from "../ai.js";
5
+
6
+ export async function summaryCommand(options) {
7
+ if (!(await isGitRepo())) {
8
+ console.log(chalk.red("❌ Not a git repository."));
9
+ process.exit(1);
10
+ }
11
+
12
+ const n = options.last || 5;
13
+ const spinner = ora(`Fetching last ${n} commits...`).start();
14
+ const commits = await getRecentCommits(n);
15
+
16
+ if (!commits.length) {
17
+ spinner.fail("No commits found.");
18
+ process.exit(1);
19
+ }
20
+ spinner.succeed(`Found ${commits.length} commits.`);
21
+
22
+ const aiSpinner = ora("Generating summary with AI...").start();
23
+
24
+ const prompt = `Summarize these git commits in plain English for a developer.
25
+
26
+ Rules:
27
+ - Group related changes together
28
+ - Use bullet points
29
+ - Be concise but informative
30
+ - Highlight the most important changes
31
+ - Use past tense
32
+
33
+ Commits:
34
+ ${commits.join("\n")}`;
35
+
36
+ try {
37
+ const summary = await askAI(prompt);
38
+ aiSpinner.succeed("Summary ready!\n");
39
+ console.log(chalk.bold(`📋 Summary of last ${n} commits:\n`));
40
+ console.log(chalk.white(summary));
41
+ console.log("");
42
+ } catch (err) {
43
+ aiSpinner.fail(chalk.red(`AI Error: ${err.message}`));
44
+ process.exit(1);
45
+ }
46
+ }
package/src/git.js ADDED
@@ -0,0 +1,43 @@
1
+ import simpleGit from 'simple-git';
2
+
3
+ const git = simpleGit();
4
+
5
+ // Get staged diff (what you've git add-ed)
6
+ export async function getStagedDiff() {
7
+ const diff = await git.diff(['--staged']);
8
+ return diff;
9
+ }
10
+
11
+ // Get diff between current branch and base branch
12
+ export async function getBranchDiff(baseBranch = 'main') {
13
+ const currentBranch = await getCurrentBranch();
14
+ const diff = await git.diff([`${baseBranch}...${currentBranch}`]);
15
+ return diff;
16
+ }
17
+
18
+ // Get last N commit messages
19
+ export async function getRecentCommits(n = 5) {
20
+ const log = await git.log({ maxCount: parseInt(n) });
21
+ return log.all.map(c => `${c.hash.slice(0, 7)} ${c.message}`);
22
+ }
23
+
24
+ // Get current branch name
25
+ export async function getCurrentBranch() {
26
+ const status = await git.status();
27
+ return status.current;
28
+ }
29
+
30
+ // Do the actual commit
31
+ export async function doCommit(message) {
32
+ await git.commit(message);
33
+ }
34
+
35
+ // Check if we're inside a git repo
36
+ export async function isGitRepo() {
37
+ try {
38
+ await git.status();
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
package/src/index.js ADDED
@@ -0,0 +1,54 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { commitCommand } from './commands/commit.js';
4
+ import { summaryCommand } from './commands/summary.js';
5
+ import { prCommand } from './commands/pr.js';
6
+ import { changelogCommand } from './commands/changelog.js';
7
+ import { configCommand } from './commands/config.js';
8
+
9
+ const program = new Command();
10
+
11
+ console.log(chalk.cyan.bold('\n🤖 GitPal — Your AI Git Assistant\n'));
12
+
13
+ program
14
+ .name('gitpal')
15
+ .description('AI-powered Git CLI — commit messages, PR descriptions, summaries & changelogs')
16
+ .version('1.0.0');
17
+
18
+ program
19
+ .command('commit')
20
+ .description('Auto-generate a commit message from your staged changes')
21
+ .option('-y, --yes', 'Skip confirmation and commit directly')
22
+ .action(commitCommand);
23
+
24
+ program
25
+ .command('summary')
26
+ .description('Summarize recent commits in plain English')
27
+ .option('-n, --last <number>', 'Number of commits to summarize', '5')
28
+ .action(summaryCommand);
29
+
30
+ program
31
+ .command('pr')
32
+ .description('Generate a pull request description from branch diff')
33
+ .option('-b, --base <branch>', 'Base branch to compare against', 'main')
34
+ .option('-c, --copy', 'Copy output to clipboard')
35
+ .action(prCommand);
36
+
37
+ program
38
+ .command('changelog')
39
+ .description('Generate a changelog from commit history')
40
+ .option('-v, --ver <version>', 'Version number for changelog', '1.0.0')
41
+ .option('-n, --last <number>', 'Number of commits to include', '20')
42
+ .action(changelogCommand);
43
+
44
+ program
45
+ .command('config')
46
+ .description('Configure your AI provider and API key')
47
+ .action(configCommand);
48
+
49
+ program.parse(process.argv);
50
+
51
+ // Show help if no command given
52
+ if (!process.argv.slice(2).length) {
53
+ program.outputHelp();
54
+ }
File without changes