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 +0 -0
- package/bin/gitpal.js +0 -0
- package/package.json +30 -0
- package/src/ai.js +110 -0
- package/src/commands/changelog.js +66 -0
- package/src/commands/commit.js +91 -0
- package/src/commands/config.js +81 -0
- package/src/commands/pr.js +72 -0
- package/src/commands/summary.js +46 -0
- package/src/git.js +43 -0
- package/src/index.js +54 -0
- package/tests/ai.test.js +0 -0
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
|
+
}
|
package/tests/ai.test.js
ADDED
|
File without changes
|