gitpt 1.0.0 → 1.2.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 +108 -20
- package/dist/commands/commit.js +25 -0
- package/dist/commands/model.d.ts +1 -0
- package/dist/commands/model.js +114 -0
- package/dist/commands/pr.d.ts +10 -0
- package/dist/commands/pr.js +458 -0
- package/dist/index.js +39 -3
- package/dist/utils/api.d.ts +1 -1
- package/dist/utils/api.js +32 -26
- package/dist/utils/commitlint.d.ts +24 -0
- package/dist/utils/commitlint.js +124 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
# GitPT
|
|
2
2
|
|
|
3
|
-
Git Prompt Tool is a CLI tool that helps you write commit messages using AI through [OpenRouter](https://openrouter.ai/).
|
|
3
|
+
Git Prompt Tool is a CLI tool that helps you write commit messages using AI through [OpenRouter](https://openrouter.ai/). It acts as a complete git wrapper, enhancing specific commands with AI while passing through all other git commands directly.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
+
- Acts as a complete git replacement - all git commands are supported
|
|
7
8
|
- Generate commit messages with AI based on your code changes
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
`gitpt commit`
|
|
11
|
+
- Create pull requests with AI-generated titles and descriptions
|
|
12
|
+
|
|
13
|
+
`gitpt pr create`
|
|
14
|
+
- Compatible with all regular git options (flags, arguments, etc.)
|
|
9
15
|
- Edit suggested messages before committing
|
|
10
16
|
- Works with various AI models via OpenRouter
|
|
17
|
+
- [Commitlint](https://commitlint.js.org/) support - read directly from your repository
|
|
11
18
|
|
|
12
19
|
## Installation
|
|
13
20
|
|
|
@@ -35,15 +42,29 @@ You'll need an [OpenRouter](https://openrouter.ai/) account to get an API key.
|
|
|
35
42
|
|
|
36
43
|
## Usage
|
|
37
44
|
|
|
38
|
-
###
|
|
45
|
+
### Using GitPT as a Complete Git Replacement
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
GitPT can be used as a direct replacement for git - any git command can be run through GitPT:
|
|
41
48
|
|
|
42
49
|
```bash
|
|
43
|
-
# Standard git
|
|
44
|
-
|
|
50
|
+
# Standard git commands work exactly the same
|
|
51
|
+
gitpt status
|
|
52
|
+
gitpt log
|
|
53
|
+
gitpt branch
|
|
54
|
+
gitpt checkout -b new-feature
|
|
55
|
+
gitpt push origin main
|
|
56
|
+
|
|
57
|
+
# GitPT passes all arguments and options to git
|
|
58
|
+
gitpt log --oneline --graph
|
|
59
|
+
gitpt merge --no-ff feature-branch
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Adding Files
|
|
45
63
|
|
|
46
|
-
|
|
64
|
+
Add files to the staging area just like you would with git:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Same as git add . (supports all the regular git add options)
|
|
47
68
|
gitpt add .
|
|
48
69
|
```
|
|
49
70
|
|
|
@@ -53,33 +74,86 @@ Generate an AI-powered commit message based on your staged changes:
|
|
|
53
74
|
|
|
54
75
|
```bash
|
|
55
76
|
gitpt commit
|
|
77
|
+
|
|
78
|
+
# Or supply -m argument, if you want to avoid gitpt generating the message
|
|
79
|
+
gitpt commit -m "feat: file hash validation"
|
|
80
|
+
|
|
81
|
+
# Pass any other git commit options
|
|
82
|
+
gitpt commit --amend
|
|
56
83
|
```
|
|
57
84
|
|
|
58
85
|
The tool will:
|
|
59
86
|
1. Analyze your staged changes
|
|
60
87
|
2. Generate a commit message using the configured AI model
|
|
61
|
-
3.
|
|
62
|
-
4.
|
|
63
|
-
5.
|
|
88
|
+
3. Validate against commitlint rules (if configured)
|
|
89
|
+
4. Regenerate the message if it fails validation
|
|
90
|
+
5. Show you the suggested message
|
|
91
|
+
6. Let you edit the message before committing
|
|
92
|
+
7. Create the commit with your approved message
|
|
64
93
|
|
|
65
|
-
###
|
|
94
|
+
### Changing Models
|
|
66
95
|
|
|
67
|
-
You can
|
|
96
|
+
You can change the AI model at any time without re-entering your API key:
|
|
68
97
|
|
|
69
98
|
```bash
|
|
70
|
-
#
|
|
71
|
-
gitpt
|
|
99
|
+
# Select model interactively (fetches available models from OpenRouter)
|
|
100
|
+
gitpt model
|
|
72
101
|
|
|
73
|
-
#
|
|
74
|
-
gitpt
|
|
102
|
+
# Specify model directly
|
|
103
|
+
gitpt model openai/gpt-4o
|
|
75
104
|
|
|
76
|
-
#
|
|
77
|
-
gitpt
|
|
105
|
+
# Switch to a different Claude model
|
|
106
|
+
gitpt model anthropic/claude-3-haiku
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## GitHub Usage
|
|
110
|
+
|
|
111
|
+
If you have GitHub CLI (`gh`) installed, you can use GitPT to interact with GitHub (e.g. generate full pull requests).
|
|
112
|
+
|
|
113
|
+
### Creating Pull Requests
|
|
114
|
+
|
|
115
|
+
Generate AI-powered pull request titles and descriptions based on your changes:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
gitpt pr create
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The tool will:
|
|
122
|
+
1. Analyze the commits and files changed since branching from the base branch
|
|
123
|
+
2. Generate a suitable PR title and detailed description
|
|
124
|
+
3. Show you the suggested content
|
|
125
|
+
4. Let you edit the title and description before submission
|
|
126
|
+
5. Create the pull request with your approved content
|
|
127
|
+
|
|
128
|
+
#### Pull Request Options
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Create a draft PR
|
|
132
|
+
gitpt pr create --draft
|
|
133
|
+
|
|
134
|
+
# Specify a custom base branch
|
|
135
|
+
gitpt pr create --base develop
|
|
136
|
+
|
|
137
|
+
# Skip editing the PR details
|
|
138
|
+
gitpt pr create --no-edit
|
|
139
|
+
|
|
140
|
+
# Provide your own title instead of generating one
|
|
141
|
+
gitpt pr create --title "Your PR title here"
|
|
78
142
|
```
|
|
79
143
|
|
|
80
144
|
## How It Works
|
|
81
145
|
|
|
82
|
-
GitPT
|
|
146
|
+
GitPT leverages AI via OpenRouter to enhance your Git workflow while acting as a complete git wrapper:
|
|
147
|
+
|
|
148
|
+
- **Command Handling:** GitPT intelligently routes commands - enhanced commands (commit, pr) use AI capabilities while all other git commands are passed directly to git.
|
|
149
|
+
|
|
150
|
+
- **For commits:** Sends a diff of your staged changes to the AI, which generates a contextual commit message following best practices.
|
|
151
|
+
|
|
152
|
+
- **Commitlint Integration:** Automatically detects commitlint configuration files and validates generated commit messages against your project's commit conventions. If validation fails, it regenerates a compliant message.
|
|
153
|
+
|
|
154
|
+
- **For pull requests:** Analyzes the commits and file changes between your branch and the base branch, then generates a suitable title and detailed description for your PR.
|
|
155
|
+
|
|
156
|
+
- **For other git commands:** Passes them through directly to git with all arguments and options preserved, ensuring complete compatibility with your existing git workflow.
|
|
83
157
|
|
|
84
158
|
## Development
|
|
85
159
|
|
|
@@ -98,6 +172,20 @@ npm run build
|
|
|
98
172
|
npm link
|
|
99
173
|
```
|
|
100
174
|
|
|
175
|
+
## Commitlint Integration
|
|
176
|
+
|
|
177
|
+
GitPT automatically detects and integrates with [commitlint](https://commitlint.js.org/) if it's configured in your repository:
|
|
178
|
+
|
|
179
|
+
- **Automatic Detection:** GitPT checks for common commitlint configuration files (commitlint.config.js, .commitlintrc.*, etc.)
|
|
180
|
+
|
|
181
|
+
- **Rule-Aware Generation:** When commitlint is detected, GitPT instructs the AI to generate messages that follow your specific commit conventions
|
|
182
|
+
|
|
183
|
+
- **Validation & Regeneration:** Generated messages are validated against your commitlint rules before committing. If validation fails, GitPT automatically regenerates a compliant message
|
|
184
|
+
|
|
185
|
+
- **Error Feedback:** Validation errors are sent to the AI to help it understand how to fix the message
|
|
186
|
+
|
|
187
|
+
This integration ensures that all AI-generated commit messages follow your team's established commit conventions without requiring manual corrections.
|
|
188
|
+
|
|
101
189
|
## License
|
|
102
190
|
|
|
103
|
-
MIT
|
|
191
|
+
MIT
|
package/dist/commands/commit.js
CHANGED
|
@@ -3,6 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { isGitRepository, hasStagedChanges, getStagedChanges, executeGitCommit } from '../utils/git.js';
|
|
4
4
|
import { generateCommitMessage } from '../utils/api.js';
|
|
5
5
|
import { getConfig } from '../utils/config.js';
|
|
6
|
+
import { hasCommitlintConfig, validateCommitMessage } from '../utils/commitlint.js';
|
|
6
7
|
export async function commitCommand(options) {
|
|
7
8
|
if (!isGitRepository()) {
|
|
8
9
|
console.error(chalk.red('Error: Not a git repository'));
|
|
@@ -29,8 +30,32 @@ export async function commitCommand(options) {
|
|
|
29
30
|
// Get staged changes
|
|
30
31
|
const diff = getStagedChanges();
|
|
31
32
|
console.log(chalk.blue('Generating commit message...'));
|
|
33
|
+
// Check if commitlint is configured
|
|
34
|
+
if (hasCommitlintConfig()) {
|
|
35
|
+
console.log(chalk.blue('Commitlint configuration detected. Generating message according to rules...'));
|
|
36
|
+
}
|
|
32
37
|
// Generate commit message
|
|
33
38
|
commitMessage = await generateCommitMessage(diff);
|
|
39
|
+
// If commitlint is configured, validate the message
|
|
40
|
+
if (hasCommitlintConfig()) {
|
|
41
|
+
console.log(chalk.blue('Validating commit message against commitlint rules...'));
|
|
42
|
+
const validation = await validateCommitMessage(commitMessage);
|
|
43
|
+
if (!validation.valid && validation.errors) {
|
|
44
|
+
console.log(chalk.yellow('Commit message failed validation. Regenerating...'));
|
|
45
|
+
console.log(chalk.gray(validation.errors));
|
|
46
|
+
// Regenerate with validation errors
|
|
47
|
+
commitMessage = await generateCommitMessage(diff, validation.errors);
|
|
48
|
+
// Validate again
|
|
49
|
+
const revalidation = await validateCommitMessage(commitMessage);
|
|
50
|
+
if (!revalidation.valid) {
|
|
51
|
+
console.log(chalk.yellow('Warning: Regenerated message still has validation issues.'));
|
|
52
|
+
console.log(chalk.gray(revalidation.errors));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
console.log(chalk.green('✓ Commit message passed validation'));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
34
59
|
console.log(chalk.green('✓ Commit message generated'));
|
|
35
60
|
console.log('');
|
|
36
61
|
console.log(chalk.cyan('Generated message:'));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function modelCommand(modelId?: string): Promise<void>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import fetch from 'node-fetch';
|
|
4
|
+
import { getConfig, saveConfig } from '../utils/config.js';
|
|
5
|
+
// List of popular models available on OpenRouter
|
|
6
|
+
const POPULAR_MODELS = [
|
|
7
|
+
{ name: 'Claude 3 Opus - Anthropic', value: 'anthropic/claude-3-opus:beta' },
|
|
8
|
+
{ name: 'Claude 3 Sonnet - Anthropic', value: 'anthropic/claude-3-sonnet:beta' },
|
|
9
|
+
{ name: 'Claude 3 Haiku - Anthropic', value: 'anthropic/claude-3-haiku:beta' },
|
|
10
|
+
{ name: 'GPT-4o - OpenAI', value: 'openai/gpt-4o' },
|
|
11
|
+
{ name: 'GPT-4 Turbo - OpenAI', value: 'openai/gpt-4-turbo' },
|
|
12
|
+
{ name: 'GPT-3.5 Turbo - OpenAI', value: 'openai/gpt-3.5-turbo' },
|
|
13
|
+
{ name: 'Other (specify model identifier)', value: 'custom' }
|
|
14
|
+
];
|
|
15
|
+
async function fetchAvailableModels(apiKey) {
|
|
16
|
+
try {
|
|
17
|
+
const response = await fetch('https://openrouter.ai/api/v1/models', {
|
|
18
|
+
headers: {
|
|
19
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
20
|
+
'HTTP-Referer': 'https://github.com/bartaxyz/GitPT',
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
|
|
25
|
+
}
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
return data.data;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.error(chalk.red('Error fetching models:'), error);
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function modelCommand(modelId) {
|
|
35
|
+
console.log(chalk.blue('GitPT Model Selection'));
|
|
36
|
+
// Check if config exists
|
|
37
|
+
const existingConfig = getConfig();
|
|
38
|
+
if (!existingConfig) {
|
|
39
|
+
console.error(chalk.red('GitPT is not configured. Please run "gitpt setup" first.'));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
let selectedModel;
|
|
43
|
+
// If a model ID is provided directly, use it
|
|
44
|
+
if (modelId) {
|
|
45
|
+
selectedModel = modelId;
|
|
46
|
+
// Update config with the new model while keeping the existing API key
|
|
47
|
+
saveConfig({
|
|
48
|
+
apiKey: existingConfig.apiKey,
|
|
49
|
+
model: selectedModel
|
|
50
|
+
});
|
|
51
|
+
console.log(chalk.green(`✓ Model set to: ${chalk.yellow(selectedModel)}`));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Otherwise, show interactive selection
|
|
55
|
+
console.log('Current model:', chalk.yellow(existingConfig.model));
|
|
56
|
+
console.log('');
|
|
57
|
+
try {
|
|
58
|
+
// Try to fetch available models from OpenRouter
|
|
59
|
+
console.log(chalk.gray('Fetching available models from OpenRouter...'));
|
|
60
|
+
const availableModels = await fetchAvailableModels(existingConfig.apiKey);
|
|
61
|
+
let modelChoices;
|
|
62
|
+
if (availableModels.length > 0) {
|
|
63
|
+
// Convert available models to choices format
|
|
64
|
+
modelChoices = availableModels.map(model => ({
|
|
65
|
+
name: `${model.name} (Context: ${model.context_length})`,
|
|
66
|
+
value: model.id
|
|
67
|
+
}));
|
|
68
|
+
// Add custom option at the end
|
|
69
|
+
modelChoices.push({ name: 'Other (specify model identifier)', value: 'custom' });
|
|
70
|
+
console.log(chalk.green(`✓ Found ${availableModels.length} models available with your API key`));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Fallback to predefined list if API call fails
|
|
74
|
+
console.log(chalk.yellow('Could not fetch models from OpenRouter, using predefined list'));
|
|
75
|
+
modelChoices = POPULAR_MODELS;
|
|
76
|
+
}
|
|
77
|
+
const answers = await inquirer.prompt([
|
|
78
|
+
{
|
|
79
|
+
type: 'list',
|
|
80
|
+
name: 'modelChoice',
|
|
81
|
+
message: 'Select an AI model:',
|
|
82
|
+
choices: modelChoices,
|
|
83
|
+
default: () => {
|
|
84
|
+
// Try to find current model in the list to set as default
|
|
85
|
+
const currentIndex = modelChoices.findIndex(choice => choice.value === existingConfig.model);
|
|
86
|
+
return currentIndex >= 0 ? currentIndex : 0;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
type: 'input',
|
|
91
|
+
name: 'customModel',
|
|
92
|
+
message: 'Enter model identifier:',
|
|
93
|
+
when: (answers) => answers.modelChoice === 'custom',
|
|
94
|
+
validate: (input) => {
|
|
95
|
+
if (!input)
|
|
96
|
+
return 'Model identifier is required';
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
]);
|
|
101
|
+
// Get the selected model
|
|
102
|
+
selectedModel = answers.modelChoice === 'custom' ? answers.customModel : answers.modelChoice;
|
|
103
|
+
// Save the updated configuration
|
|
104
|
+
saveConfig({
|
|
105
|
+
apiKey: existingConfig.apiKey,
|
|
106
|
+
model: selectedModel
|
|
107
|
+
});
|
|
108
|
+
console.log(chalk.green(`✓ Model updated to: ${chalk.yellow(selectedModel)}`));
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
console.error(chalk.red('Error updating model:'), error);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { isGitRepository } from '../utils/git.js';
|
|
5
|
+
import { getConfig } from '../utils/config.js';
|
|
6
|
+
import fetch from 'node-fetch';
|
|
7
|
+
function getCurrentBranch() {
|
|
8
|
+
try {
|
|
9
|
+
return execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
console.error(chalk.red('Error getting current branch:'), error);
|
|
13
|
+
throw new Error('Failed to get current branch');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function getDefaultBaseBranch() {
|
|
17
|
+
try {
|
|
18
|
+
// First check for a default branch set in git config
|
|
19
|
+
try {
|
|
20
|
+
const defaultBranch = execSync('git config init.defaultBranch').toString().trim();
|
|
21
|
+
if (defaultBranch) {
|
|
22
|
+
return defaultBranch;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
// Continue if git config doesn't have default branch
|
|
27
|
+
}
|
|
28
|
+
// Next, check if GitHub CLI can tell us the default branch
|
|
29
|
+
try {
|
|
30
|
+
const repoInfo = execSync('gh repo view --json defaultBranchRef --jq .defaultBranchRef.name').toString().trim();
|
|
31
|
+
if (repoInfo) {
|
|
32
|
+
return repoInfo;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
// Continue if gh command fails
|
|
37
|
+
}
|
|
38
|
+
// Try to find default branch in remote branches list
|
|
39
|
+
const branches = execSync('git branch -r').toString().trim().split('\n');
|
|
40
|
+
// Common default branch names, in order of likelihood
|
|
41
|
+
const mainPatterns = [
|
|
42
|
+
/origin\/main$/,
|
|
43
|
+
/origin\/master$/,
|
|
44
|
+
/origin\/develop$/,
|
|
45
|
+
/origin\/dev$/,
|
|
46
|
+
/origin\/trunk$/
|
|
47
|
+
];
|
|
48
|
+
// Try each pattern in order
|
|
49
|
+
for (const pattern of mainPatterns) {
|
|
50
|
+
const defaultBranch = branches.find(b => pattern.test(b.trim()));
|
|
51
|
+
if (defaultBranch) {
|
|
52
|
+
return defaultBranch.trim().replace(/^origin\//, '');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Check for a branch that has 'HEAD -> origin/' in it, indicating the default branch
|
|
56
|
+
const headBranch = branches.find(b => b.includes('HEAD -> origin/'));
|
|
57
|
+
if (headBranch) {
|
|
58
|
+
const match = headBranch.match(/HEAD -> origin\/([^,\s]+)/);
|
|
59
|
+
if (match && match[1]) {
|
|
60
|
+
return match[1];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Fallback to 'main' as most GitHub repos use this now
|
|
64
|
+
console.log(chalk.yellow('Could not determine default branch, using "main"'));
|
|
65
|
+
return 'main';
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
// If we can't determine, default to main
|
|
69
|
+
console.log(chalk.yellow('Error detecting default branch, using "main"'));
|
|
70
|
+
return 'main';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function getCommitsSinceBaseBranch(baseBranch) {
|
|
74
|
+
try {
|
|
75
|
+
// Try first with origin/baseBranch
|
|
76
|
+
try {
|
|
77
|
+
const mergeBase = execSync(`git merge-base HEAD origin/${baseBranch}`).toString().trim();
|
|
78
|
+
const commitMessages = execSync(`git log --pretty=format:"%s" ${mergeBase}..HEAD`).toString().trim();
|
|
79
|
+
if (commitMessages) {
|
|
80
|
+
return commitMessages.split('\n').filter(Boolean);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
// If origin/baseBranch doesn't exist, try with just baseBranch
|
|
85
|
+
console.log(chalk.yellow(`No origin/${baseBranch} found, trying with local ${baseBranch} branch...`));
|
|
86
|
+
}
|
|
87
|
+
// Try with local branch
|
|
88
|
+
try {
|
|
89
|
+
const mergeBase = execSync(`git merge-base HEAD ${baseBranch}`).toString().trim();
|
|
90
|
+
const commitMessages = execSync(`git log --pretty=format:"%s" ${mergeBase}..HEAD`).toString().trim();
|
|
91
|
+
if (commitMessages) {
|
|
92
|
+
return commitMessages.split('\n').filter(Boolean);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
// If that fails too, fallback to simple branch comparison
|
|
97
|
+
console.log(chalk.yellow(`Merge base with ${baseBranch} not found, comparing branches directly...`));
|
|
98
|
+
}
|
|
99
|
+
// Direct branch comparison
|
|
100
|
+
try {
|
|
101
|
+
const commitMessages = execSync(`git log --pretty=format:"%s" ${baseBranch}..HEAD`).toString().trim();
|
|
102
|
+
if (commitMessages) {
|
|
103
|
+
return commitMessages.split('\n').filter(Boolean);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.log(chalk.yellow(`Could not compare with ${baseBranch}, using recent commits...`));
|
|
108
|
+
}
|
|
109
|
+
// Last resort: get most recent commits
|
|
110
|
+
const commitMessages = execSync('git log --pretty=format:"%s" -n 10').toString().trim();
|
|
111
|
+
return commitMessages.split('\n').filter(Boolean);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
console.error(chalk.yellow('Could not get commits. Using empty list.'));
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function getChangedFiles(baseBranch) {
|
|
119
|
+
// Try several methods to get changed files
|
|
120
|
+
// Method 1: Compare with origin/baseBranch using three dots
|
|
121
|
+
try {
|
|
122
|
+
const changedFiles = execSync(`git diff --name-only origin/${baseBranch}...HEAD`).toString().trim();
|
|
123
|
+
if (changedFiles) {
|
|
124
|
+
return changedFiles.split('\n').filter(Boolean);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
// Continue to next method
|
|
129
|
+
}
|
|
130
|
+
// Method 2: Compare with local baseBranch using three dots
|
|
131
|
+
try {
|
|
132
|
+
const changedFiles = execSync(`git diff --name-only ${baseBranch}...HEAD`).toString().trim();
|
|
133
|
+
if (changedFiles) {
|
|
134
|
+
return changedFiles.split('\n').filter(Boolean);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
// Continue to next method
|
|
139
|
+
}
|
|
140
|
+
// Method 3: Direct comparison with two dots
|
|
141
|
+
try {
|
|
142
|
+
const changedFiles = execSync(`git diff --name-only ${baseBranch}..HEAD`).toString().trim();
|
|
143
|
+
if (changedFiles) {
|
|
144
|
+
return changedFiles.split('\n').filter(Boolean);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
// Continue to next method
|
|
149
|
+
}
|
|
150
|
+
// Method 4: Get recently modified files
|
|
151
|
+
try {
|
|
152
|
+
console.log(chalk.yellow(`Could not determine changed files relative to ${baseBranch}, using recently modified files...`));
|
|
153
|
+
const changedFiles = execSync('git ls-files --modified --others --exclude-standard').toString().trim();
|
|
154
|
+
if (changedFiles) {
|
|
155
|
+
return changedFiles.split('\n').filter(Boolean);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
// Last resort
|
|
160
|
+
}
|
|
161
|
+
// Method 5: List all files in the repo as a last resort
|
|
162
|
+
try {
|
|
163
|
+
console.log(chalk.yellow('Using all tracked files as fallback...'));
|
|
164
|
+
const allFiles = execSync('git ls-files').toString().trim();
|
|
165
|
+
return allFiles.split('\n').filter(Boolean).slice(0, 50); // Limit to first 50 files
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
console.error(chalk.red('Could not determine changed files.'));
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function generatePRDetails(baseBranch, currentBranch) {
|
|
173
|
+
const config = getConfig();
|
|
174
|
+
if (!config) {
|
|
175
|
+
throw new Error('GitPT is not configured. Please run "gitpt setup" first.');
|
|
176
|
+
}
|
|
177
|
+
const { apiKey, model } = config;
|
|
178
|
+
// Get context for PR
|
|
179
|
+
const commitMessages = getCommitsSinceBaseBranch(baseBranch);
|
|
180
|
+
const changedFiles = getChangedFiles(baseBranch);
|
|
181
|
+
// Check if we have any content to work with
|
|
182
|
+
if (commitMessages.length === 0 && changedFiles.length === 0) {
|
|
183
|
+
console.log(chalk.yellow('No commits or changed files detected.'));
|
|
184
|
+
console.log(chalk.yellow('Will attempt to generate PR details using branch name and repository context.'));
|
|
185
|
+
}
|
|
186
|
+
// Get additional context from repository
|
|
187
|
+
let repoName = "";
|
|
188
|
+
let repoDescription = "";
|
|
189
|
+
try {
|
|
190
|
+
// Try to get repo information from GitHub CLI
|
|
191
|
+
const repoInfo = JSON.parse(execSync('gh repo view --json name,description').toString().trim());
|
|
192
|
+
repoName = repoInfo.name || "";
|
|
193
|
+
repoDescription = repoInfo.description || "";
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
// Continue without this info
|
|
197
|
+
}
|
|
198
|
+
console.log(chalk.blue('Generating PR title and description...'));
|
|
199
|
+
// Build a rich context for the AI
|
|
200
|
+
let contextSections = [
|
|
201
|
+
`Branch: ${currentBranch}`,
|
|
202
|
+
`Base branch: ${baseBranch}`
|
|
203
|
+
];
|
|
204
|
+
// Add repository info if available
|
|
205
|
+
if (repoName) {
|
|
206
|
+
contextSections.push(`Repository: ${repoName}`);
|
|
207
|
+
}
|
|
208
|
+
if (repoDescription) {
|
|
209
|
+
contextSections.push(`Repository description: ${repoDescription}`);
|
|
210
|
+
}
|
|
211
|
+
// Add commit messages if available
|
|
212
|
+
if (commitMessages.length > 0) {
|
|
213
|
+
contextSections.push('Commit messages in this branch:', commitMessages.map(msg => `- ${msg}`).join('\n'));
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
contextSections.push('No commit messages available.');
|
|
217
|
+
// Try to extract intent from branch name if no commits
|
|
218
|
+
if (currentBranch.includes('/')) {
|
|
219
|
+
const branchParts = currentBranch.split('/');
|
|
220
|
+
const branchType = branchParts[0]; // e.g., "feature", "fix", "chore"
|
|
221
|
+
const branchDescription = branchParts.slice(1).join('/').replace(/-/g, ' ');
|
|
222
|
+
contextSections.push('Branch name analysis:', `Type: ${branchType}`, `Description: ${branchDescription}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Add changed files if available
|
|
226
|
+
if (changedFiles.length > 0) {
|
|
227
|
+
contextSections.push('Files changed in this branch:', changedFiles.map(file => `- ${file}`).join('\n'));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
contextSections.push('No file changes detected.');
|
|
231
|
+
}
|
|
232
|
+
// Create the final context
|
|
233
|
+
const context = contextSections.join('\n\n');
|
|
234
|
+
const systemPrompt = `You are a helpful assistant that generates clear, informative GitHub pull request titles and descriptions.
|
|
235
|
+
For the title:
|
|
236
|
+
- Keep it concise (under 80 characters)
|
|
237
|
+
- Start with a verb in present tense (e.g., "Add", "Fix", "Update")
|
|
238
|
+
- Clearly summarize the main purpose of the changes
|
|
239
|
+
|
|
240
|
+
For the description:
|
|
241
|
+
- Start with a brief summary (1-2 sentences) of what the PR accomplishes
|
|
242
|
+
- Include a more detailed explanation of changes if needed
|
|
243
|
+
- List key changes as bullet points if there are multiple components
|
|
244
|
+
- Include any relevant context that reviewers should know
|
|
245
|
+
- End with any testing instructions if applicable
|
|
246
|
+
|
|
247
|
+
Format the description in Markdown with sections.
|
|
248
|
+
Do not include "PR" or "Pull Request" in the title.`;
|
|
249
|
+
const userPrompt = `Generate a pull request title and description for the following changes:
|
|
250
|
+
|
|
251
|
+
${context}
|
|
252
|
+
|
|
253
|
+
Format your response exactly like this example:
|
|
254
|
+
Title: Add user authentication with JWT
|
|
255
|
+
Description:
|
|
256
|
+
## Summary
|
|
257
|
+
This PR adds user authentication using JWT tokens.
|
|
258
|
+
|
|
259
|
+
## Changes
|
|
260
|
+
- Implement login and registration endpoints
|
|
261
|
+
- Add JWT generation and validation
|
|
262
|
+
- Update user model with password hashing
|
|
263
|
+
- Add authorization middleware
|
|
264
|
+
|
|
265
|
+
## How to test
|
|
266
|
+
1. Register a new user with \`/api/register\`
|
|
267
|
+
2. Login with the new user credentials
|
|
268
|
+
3. Use the returned token to access protected endpoints`;
|
|
269
|
+
try {
|
|
270
|
+
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
271
|
+
method: 'POST',
|
|
272
|
+
headers: {
|
|
273
|
+
'Content-Type': 'application/json',
|
|
274
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
275
|
+
'HTTP-Referer': 'https://github.com/bartaxyz/GitPT',
|
|
276
|
+
},
|
|
277
|
+
body: JSON.stringify({
|
|
278
|
+
model: model,
|
|
279
|
+
messages: [
|
|
280
|
+
{ role: 'system', content: systemPrompt },
|
|
281
|
+
{ role: 'user', content: userPrompt }
|
|
282
|
+
],
|
|
283
|
+
max_tokens: 1000,
|
|
284
|
+
}),
|
|
285
|
+
});
|
|
286
|
+
if (!response.ok) {
|
|
287
|
+
const errorText = await response.text();
|
|
288
|
+
throw new Error(`API request failed: ${response.status} ${response.statusText}\n${errorText}`);
|
|
289
|
+
}
|
|
290
|
+
const data = await response.json();
|
|
291
|
+
const result = data.choices[0].message.content.trim();
|
|
292
|
+
// Parse title and description from AI response
|
|
293
|
+
const titleMatch = result.match(/Title:\s*(.+?)(?:\n|$)/);
|
|
294
|
+
const descMatch = result.match(/Description:\s*\n([\s\S]+)$/);
|
|
295
|
+
const title = titleMatch ? titleMatch[1].trim() : '';
|
|
296
|
+
const body = descMatch ? descMatch[1].trim() : result; // Fallback to full response if parsing fails
|
|
297
|
+
return { title, body };
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
console.error(chalk.red('Error generating PR details:'), error);
|
|
301
|
+
throw new Error('Failed to generate PR details');
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function checkGitHubCLIAvailability() {
|
|
305
|
+
try {
|
|
306
|
+
execSync('gh --version', { stdio: 'ignore' });
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function createPullRequest(title, body, baseBranch, draft) {
|
|
314
|
+
const draftFlag = draft ? '--draft' : '';
|
|
315
|
+
try {
|
|
316
|
+
console.log(chalk.blue(`Creating pull request to ${baseBranch}...`));
|
|
317
|
+
// Create a temporary file for the PR body to avoid issues with escaping
|
|
318
|
+
const tempFilePath = `/tmp/gitpt-pr-body-${Date.now()}.md`;
|
|
319
|
+
try {
|
|
320
|
+
// Write the body to a temporary file
|
|
321
|
+
execSync(`cat > "${tempFilePath}" << 'GITPT_EOF'
|
|
322
|
+
${body}
|
|
323
|
+
GITPT_EOF`);
|
|
324
|
+
// Try to get the remote repo URL if available
|
|
325
|
+
let repoUrlArg = '';
|
|
326
|
+
try {
|
|
327
|
+
const repoUrl = execSync('git config --get remote.origin.url').toString().trim();
|
|
328
|
+
if (repoUrl) {
|
|
329
|
+
repoUrlArg = `--repo "${repoUrl}"`;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
// Proceed without repo URL
|
|
334
|
+
}
|
|
335
|
+
// Use the file for the body
|
|
336
|
+
const command = `gh pr create --title "${title.replace(/"/g, '\\"')}" --body-file "${tempFilePath}" --base "${baseBranch}" ${draftFlag} ${repoUrlArg}`;
|
|
337
|
+
// Set a timeout to avoid hanging indefinitely
|
|
338
|
+
console.log(chalk.gray('Running GitHub PR creation command...'));
|
|
339
|
+
console.log(chalk.gray(`Using base branch: ${baseBranch}`));
|
|
340
|
+
// Add debugging output
|
|
341
|
+
console.log(chalk.gray('Executing command with 60s timeout:'));
|
|
342
|
+
// Execute the command with a timeout
|
|
343
|
+
const result = execSync(command, {
|
|
344
|
+
stdio: 'pipe',
|
|
345
|
+
timeout: 60000 // 60-second timeout
|
|
346
|
+
}).toString();
|
|
347
|
+
console.log(result);
|
|
348
|
+
console.log(chalk.green('✓ Pull request created successfully'));
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
// Clean up temporary file
|
|
352
|
+
try {
|
|
353
|
+
execSync(`rm -f "${tempFilePath}"`);
|
|
354
|
+
}
|
|
355
|
+
catch (e) {
|
|
356
|
+
// Ignore cleanup errors
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
if (error instanceof Error && error.message.includes('timeout')) {
|
|
362
|
+
console.error(chalk.red('Error: GitHub CLI command timed out after 60 seconds.'));
|
|
363
|
+
console.log(chalk.yellow('You may need to create the PR manually using:'));
|
|
364
|
+
console.log(chalk.yellow(`gh pr create --title "${title}" --base "${baseBranch}" ${draftFlag}`));
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
console.error(chalk.red('Error creating pull request:'), error);
|
|
368
|
+
}
|
|
369
|
+
throw new Error('Failed to create pull request');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
export async function prCreateCommand(options = {}) {
|
|
373
|
+
if (!isGitRepository()) {
|
|
374
|
+
console.error(chalk.red('Error: Not a git repository'));
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
// Check if GitHub CLI is available
|
|
378
|
+
if (!checkGitHubCLIAvailability()) {
|
|
379
|
+
console.error(chalk.red('Error: GitHub CLI (gh) is not installed or not available in PATH.'));
|
|
380
|
+
console.log(chalk.yellow('Please install GitHub CLI from https://cli.github.com/'));
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
// Check if user is authenticated with GitHub CLI
|
|
384
|
+
try {
|
|
385
|
+
const authStatus = execSync('gh auth status -h github.com 2>&1 || true').toString();
|
|
386
|
+
if (authStatus.includes('not logged')) {
|
|
387
|
+
console.error(chalk.red('Error: You are not authenticated with GitHub CLI.'));
|
|
388
|
+
console.log(chalk.yellow('Please run `gh auth login` to authenticate.'));
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
console.log(chalk.yellow('Warning: Could not verify GitHub CLI authentication.'));
|
|
394
|
+
console.log(chalk.yellow('If PR creation fails, please run `gh auth login` first.'));
|
|
395
|
+
}
|
|
396
|
+
// Get configuration
|
|
397
|
+
const config = getConfig();
|
|
398
|
+
if (!config) {
|
|
399
|
+
console.error(chalk.red('GitPT is not configured. Please run "gitpt setup" first.'));
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
const currentBranch = getCurrentBranch();
|
|
404
|
+
const baseBranch = options.base || getDefaultBaseBranch();
|
|
405
|
+
let title = options.title || '';
|
|
406
|
+
let body = options.body || '';
|
|
407
|
+
// Generate PR details if not provided
|
|
408
|
+
if (!title || !body) {
|
|
409
|
+
const generatedDetails = await generatePRDetails(baseBranch, currentBranch);
|
|
410
|
+
title = title || generatedDetails.title;
|
|
411
|
+
body = body || generatedDetails.body;
|
|
412
|
+
console.log(chalk.green('✓ PR details generated'));
|
|
413
|
+
console.log('');
|
|
414
|
+
console.log(chalk.cyan('Generated title:'));
|
|
415
|
+
console.log(title);
|
|
416
|
+
console.log('');
|
|
417
|
+
console.log(chalk.cyan('Generated description:'));
|
|
418
|
+
console.log(body);
|
|
419
|
+
console.log('');
|
|
420
|
+
}
|
|
421
|
+
// Allow editing PR details
|
|
422
|
+
if (options.edit !== false) {
|
|
423
|
+
const answers = await inquirer.prompt([
|
|
424
|
+
{
|
|
425
|
+
type: 'input',
|
|
426
|
+
name: 'title',
|
|
427
|
+
message: 'Edit PR title:',
|
|
428
|
+
default: title
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
type: 'editor',
|
|
432
|
+
name: 'body',
|
|
433
|
+
message: 'Edit PR description:',
|
|
434
|
+
default: body
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
type: 'confirm',
|
|
438
|
+
name: 'draft',
|
|
439
|
+
message: 'Create as draft PR?',
|
|
440
|
+
default: options.draft || false
|
|
441
|
+
}
|
|
442
|
+
]);
|
|
443
|
+
title = answers.title;
|
|
444
|
+
body = answers.body;
|
|
445
|
+
const isDraft = answers.draft;
|
|
446
|
+
// Create the PR
|
|
447
|
+
createPullRequest(title, body, baseBranch, isDraft);
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
// Create the PR without editing
|
|
451
|
+
createPullRequest(title, body, baseBranch, options.draft || false);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
4
5
|
import { setupCommand } from './commands/setup.js';
|
|
5
6
|
import { commitCommand } from './commands/commit.js';
|
|
6
7
|
import { addCommand } from './commands/add.js';
|
|
8
|
+
import { modelCommand } from './commands/model.js';
|
|
9
|
+
import { prCreateCommand } from './commands/pr.js';
|
|
7
10
|
import fs from 'fs';
|
|
8
11
|
import path from 'path';
|
|
9
12
|
import { fileURLToPath } from 'url';
|
|
13
|
+
import { isGitRepository } from './utils/git.js';
|
|
10
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
15
|
const __dirname = path.dirname(__filename);
|
|
12
16
|
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8'));
|
|
@@ -16,17 +20,21 @@ program
|
|
|
16
20
|
.name('gitpt')
|
|
17
21
|
.description('Git Prompt Tool helps you write commit messages using AI')
|
|
18
22
|
.version(version);
|
|
19
|
-
//
|
|
23
|
+
// GitPT-specific commands
|
|
20
24
|
program
|
|
21
25
|
.command('setup')
|
|
22
26
|
.description('Configure GitPT with your OpenRouter API key and model selection')
|
|
23
27
|
.action(setupCommand);
|
|
24
|
-
|
|
28
|
+
program
|
|
29
|
+
.command('model [model-id]')
|
|
30
|
+
.description('Change the AI model used for generating commit messages')
|
|
31
|
+
.action(modelCommand);
|
|
32
|
+
// Enhanced git commands
|
|
25
33
|
program
|
|
26
34
|
.command('add [files...]')
|
|
27
35
|
.description('Add files to git staging area (pass-through to git add)')
|
|
36
|
+
.allowUnknownOption(true) // Pass through other git add options
|
|
28
37
|
.action(addCommand);
|
|
29
|
-
// Commit command
|
|
30
38
|
program
|
|
31
39
|
.command('commit')
|
|
32
40
|
.description('Generate AI-powered commit message based on staged changes')
|
|
@@ -35,6 +43,34 @@ program
|
|
|
35
43
|
.option('--no-edit', 'do not edit the message after generation')
|
|
36
44
|
.allowUnknownOption(true) // Pass through other git commit options
|
|
37
45
|
.action(commitCommand);
|
|
46
|
+
program
|
|
47
|
+
.command('pr create')
|
|
48
|
+
.description('Create a pull request with AI-generated title and description')
|
|
49
|
+
.option('-t, --title <title>', 'Custom pull request title')
|
|
50
|
+
.option('-b, --body <body>', 'Custom pull request description')
|
|
51
|
+
.option('-d, --draft', 'Create as draft pull request')
|
|
52
|
+
.option('-B, --base <branch>', 'Base branch to create PR against')
|
|
53
|
+
.option('-e, --edit', 'Edit PR details before submission', true)
|
|
54
|
+
.option('--no-edit', 'Skip editing PR details')
|
|
55
|
+
.allowUnknownOption(true)
|
|
56
|
+
.action(prCreateCommand);
|
|
57
|
+
// Handle unknown commands by passing them to git
|
|
58
|
+
program.on('command:*', (operands) => {
|
|
59
|
+
if (!isGitRepository()) {
|
|
60
|
+
console.error(chalk.red('Error: Not a git repository'));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
// Get all arguments passed to the original command
|
|
65
|
+
const args = process.argv.slice(2);
|
|
66
|
+
// Execute git with all arguments
|
|
67
|
+
execSync(`git ${args.join(' ')}`, { stdio: 'inherit' });
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
// Git will handle its own error output through stdio: 'inherit'
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
38
74
|
// Main logic
|
|
39
75
|
async function main() {
|
|
40
76
|
try {
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function generateCommitMessage(diff: string): Promise<string>;
|
|
1
|
+
export declare function generateCommitMessage(diff: string, validationErrors?: string): Promise<string>;
|
package/dist/utils/api.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import fetch from
|
|
2
|
-
import { getConfig } from
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import fetch from "node-fetch";
|
|
2
|
+
import { getConfig } from "./config.js";
|
|
3
|
+
import { hasCommitlintConfig, getCommitlintRules } from "./commitlint.js";
|
|
4
|
+
const OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions";
|
|
5
|
+
export async function generateCommitMessage(diff, validationErrors) {
|
|
6
|
+
// Check if commitlint is configured
|
|
7
|
+
const hasCommitlint = hasCommitlintConfig();
|
|
5
8
|
const config = getConfig();
|
|
6
9
|
if (!config) {
|
|
7
10
|
throw new Error('GitPT is not configured. Please run "gitpt setup" first.');
|
|
@@ -9,31 +12,34 @@ export async function generateCommitMessage(diff) {
|
|
|
9
12
|
const { apiKey, model } = config;
|
|
10
13
|
const messages = [
|
|
11
14
|
{
|
|
12
|
-
role:
|
|
13
|
-
content: `You are a helpful assistant that generates concise, informative Git commit messages.
|
|
14
|
-
Follow
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
role: "system",
|
|
16
|
+
content: `You are a helpful assistant that generates concise, informative Git commit messages.
|
|
17
|
+
Follow these strict rules:
|
|
18
|
+
${hasCommitlint ? getCommitlintRules() : `1. Use conventional commit format: type: description
|
|
19
|
+
2. Types are: feat, fix, docs, style, refactor, test, chore
|
|
20
|
+
3. NO scopes in parentheses - do not use feat(scope)
|
|
21
|
+
4. Keep the entire message under 100 characters
|
|
22
|
+
5. Use present tense (e.g., "add feature" not "added feature")
|
|
23
|
+
6. Be brief but descriptive about WHAT changed
|
|
24
|
+
7. Do not include detailed explanations
|
|
25
|
+
8. Examples:
|
|
26
|
+
- feat: add user authentication
|
|
27
|
+
- fix: resolve null pointer in login
|
|
28
|
+
- chore: update dependencies
|
|
29
|
+
- style: format css files`}${validationErrors ? `\n\nYOUR PREVIOUS MESSAGE FAILED VALIDATION WITH THESE ERRORS:\n${validationErrors}\n\nFIX THESE ISSUES IN YOUR NEW MESSAGE.` : ''}`,
|
|
24
30
|
},
|
|
25
31
|
{
|
|
26
|
-
role:
|
|
27
|
-
content: `Generate a commit message for the following git diff:\n\n${diff}
|
|
28
|
-
}
|
|
32
|
+
role: "user",
|
|
33
|
+
content: `Generate a commit message for the following git diff:\n\n${diff}`,
|
|
34
|
+
},
|
|
29
35
|
];
|
|
30
36
|
try {
|
|
31
37
|
const response = await fetch(OPENROUTER_API_URL, {
|
|
32
|
-
method:
|
|
38
|
+
method: "POST",
|
|
33
39
|
headers: {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
Authorization: `Bearer ${apiKey}`,
|
|
42
|
+
"HTTP-Referer": "https://github.com/bartaxyz/GitPT",
|
|
37
43
|
},
|
|
38
44
|
body: JSON.stringify({
|
|
39
45
|
model: model,
|
|
@@ -45,11 +51,11 @@ export async function generateCommitMessage(diff) {
|
|
|
45
51
|
const errorText = await response.text();
|
|
46
52
|
throw new Error(`API request failed: ${response.status} ${response.statusText}\n${errorText}`);
|
|
47
53
|
}
|
|
48
|
-
const data = await response.json();
|
|
54
|
+
const data = (await response.json());
|
|
49
55
|
return data.choices[0].message.content.trim();
|
|
50
56
|
}
|
|
51
57
|
catch (error) {
|
|
52
|
-
console.error(
|
|
53
|
-
throw new Error(
|
|
58
|
+
console.error("Error generating commit message:", error);
|
|
59
|
+
throw new Error("Failed to generate commit message");
|
|
54
60
|
}
|
|
55
61
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a parsed commitlint configuration
|
|
3
|
+
*/
|
|
4
|
+
export interface CommitlintConfig {
|
|
5
|
+
rules?: Record<string, any>;
|
|
6
|
+
extends?: string | string[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Check if a commitlint configuration exists in the repository
|
|
10
|
+
*/
|
|
11
|
+
export declare function hasCommitlintConfig(): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Get the commitlint configuration format rules as a string
|
|
14
|
+
*/
|
|
15
|
+
export declare function getCommitlintRules(): string;
|
|
16
|
+
/**
|
|
17
|
+
* Validate a commit message against commitlint rules
|
|
18
|
+
* @param message The commit message to validate
|
|
19
|
+
* @returns An object with success status and error message if applicable
|
|
20
|
+
*/
|
|
21
|
+
export declare function validateCommitMessage(message: string): Promise<{
|
|
22
|
+
valid: boolean;
|
|
23
|
+
errors?: string;
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
/**
|
|
4
|
+
* Check if a commitlint configuration exists in the repository
|
|
5
|
+
*/
|
|
6
|
+
export function hasCommitlintConfig() {
|
|
7
|
+
const possibleConfigFiles = [
|
|
8
|
+
'.commitlintrc',
|
|
9
|
+
'.commitlintrc.json',
|
|
10
|
+
'.commitlintrc.yaml',
|
|
11
|
+
'.commitlintrc.yml',
|
|
12
|
+
'.commitlintrc.js',
|
|
13
|
+
'commitlint.config.js',
|
|
14
|
+
'package.json'
|
|
15
|
+
];
|
|
16
|
+
// Check if any of the possible config files exist
|
|
17
|
+
return possibleConfigFiles.some(file => {
|
|
18
|
+
try {
|
|
19
|
+
if (file === 'package.json') {
|
|
20
|
+
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
21
|
+
return packageJson.commitlint !== undefined;
|
|
22
|
+
}
|
|
23
|
+
return fs.existsSync(file);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get the commitlint configuration format rules as a string
|
|
32
|
+
*/
|
|
33
|
+
export function getCommitlintRules() {
|
|
34
|
+
try {
|
|
35
|
+
// Try to get the rules using commitlint CLI if available
|
|
36
|
+
try {
|
|
37
|
+
const rulesOutput = execSync('npx commitlint --print-config', { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
38
|
+
const config = JSON.parse(rulesOutput);
|
|
39
|
+
return formatCommitlintRules(config);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
// If CLI approach fails, try to find and parse config files manually
|
|
43
|
+
// This is a simplified approach - in a real implementation, you'd want to handle
|
|
44
|
+
// all possible config files and formats
|
|
45
|
+
if (fs.existsSync('commitlint.config.js')) {
|
|
46
|
+
// We can't directly require the JS file in ESM, so we'll return a generic message
|
|
47
|
+
return "Follow the conventional commit format (type(scope): message)";
|
|
48
|
+
}
|
|
49
|
+
if (fs.existsSync('.commitlintrc.json')) {
|
|
50
|
+
const config = JSON.parse(fs.readFileSync('.commitlintrc.json', 'utf8'));
|
|
51
|
+
return formatCommitlintRules(config);
|
|
52
|
+
}
|
|
53
|
+
// Default to conventional commits format if we can't parse the config
|
|
54
|
+
return "Follow the conventional commit format (type(scope): message)";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
// If all else fails, return a generic message
|
|
59
|
+
return "Follow the conventional commit format (type(scope): message)";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Format commitlint rules into a human-readable string
|
|
64
|
+
*/
|
|
65
|
+
function formatCommitlintRules(config) {
|
|
66
|
+
let rulesDescription = "Follow these commit message rules:\n";
|
|
67
|
+
if (!config.rules) {
|
|
68
|
+
return "Follow the conventional commit format (type(scope): message)";
|
|
69
|
+
}
|
|
70
|
+
// Extract type-enum rule if it exists
|
|
71
|
+
if (config.rules['type-enum'] && Array.isArray(config.rules['type-enum'][2])) {
|
|
72
|
+
const allowedTypes = config.rules['type-enum'][2];
|
|
73
|
+
rulesDescription += `- Commit type must be one of: ${allowedTypes.join(', ')}\n`;
|
|
74
|
+
}
|
|
75
|
+
// Extract other common rules
|
|
76
|
+
if (config.rules['scope-enum'] && Array.isArray(config.rules['scope-enum'][2])) {
|
|
77
|
+
const allowedScopes = config.rules['scope-enum'][2];
|
|
78
|
+
rulesDescription += `- Scope must be one of: ${allowedScopes.join(', ')}\n`;
|
|
79
|
+
}
|
|
80
|
+
if (config.rules['subject-case']) {
|
|
81
|
+
rulesDescription += `- Subject must follow case rules\n`;
|
|
82
|
+
}
|
|
83
|
+
if (config.rules['subject-max-length']) {
|
|
84
|
+
const maxLength = config.rules['subject-max-length'][2];
|
|
85
|
+
rulesDescription += `- Subject must be no longer than ${maxLength} characters\n`;
|
|
86
|
+
}
|
|
87
|
+
return rulesDescription;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Validate a commit message against commitlint rules
|
|
91
|
+
* @param message The commit message to validate
|
|
92
|
+
* @returns An object with success status and error message if applicable
|
|
93
|
+
*/
|
|
94
|
+
export async function validateCommitMessage(message) {
|
|
95
|
+
if (!hasCommitlintConfig()) {
|
|
96
|
+
// If no commitlint config, consider it valid
|
|
97
|
+
return { valid: true };
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
// Create a temporary file with the message
|
|
101
|
+
const tempFile = `/tmp/gitpt-commit-msg-${Date.now()}`;
|
|
102
|
+
fs.writeFileSync(tempFile, message);
|
|
103
|
+
try {
|
|
104
|
+
// Run commitlint against the file
|
|
105
|
+
execSync(`npx commitlint --config commitlint.config.js < ${tempFile}`, { stdio: 'ignore' });
|
|
106
|
+
// If we get here, validation passed
|
|
107
|
+
fs.unlinkSync(tempFile); // Clean up temp file
|
|
108
|
+
return { valid: true };
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
// Capture the error output for feedback
|
|
112
|
+
const errorOutput = execSync(`npx commitlint --config commitlint.config.js < ${tempFile} 2>&1 || true`).toString();
|
|
113
|
+
fs.unlinkSync(tempFile); // Clean up temp file
|
|
114
|
+
return {
|
|
115
|
+
valid: false,
|
|
116
|
+
errors: errorOutput
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
// If we can't run commitlint, consider it valid to avoid blocking
|
|
122
|
+
return { valid: true };
|
|
123
|
+
}
|
|
124
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitpt",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "CLI tool that helps you write commit messages & pull request descriptions using AI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -48,5 +48,9 @@
|
|
|
48
48
|
"configstore": "^7.0.0",
|
|
49
49
|
"inquirer": "^12.5.2",
|
|
50
50
|
"node-fetch": "^3.3.2"
|
|
51
|
+
},
|
|
52
|
+
"optionalDependencies": {
|
|
53
|
+
"@commitlint/cli": "^18.4.3",
|
|
54
|
+
"@commitlint/config-conventional": "^18.4.3"
|
|
51
55
|
}
|
|
52
56
|
}
|