gitpt 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -9
- 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.js +28 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
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
|
+
- Create pull requests with AI-generated titles and descriptions
|
|
10
|
+
- Compatible with all regular git options (flags, arguments, etc.)
|
|
9
11
|
- Edit suggested messages before committing
|
|
10
12
|
- Works with various AI models via OpenRouter
|
|
11
13
|
|
|
@@ -35,16 +37,36 @@ You'll need an [OpenRouter](https://openrouter.ai/) account to get an API key.
|
|
|
35
37
|
|
|
36
38
|
## Usage
|
|
37
39
|
|
|
38
|
-
###
|
|
40
|
+
### Using GitPT as a Complete Git Replacement
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
GitPT can be used as a direct replacement for git - any git command can be run through GitPT:
|
|
41
43
|
|
|
42
44
|
```bash
|
|
43
|
-
# Standard git
|
|
44
|
-
|
|
45
|
+
# Standard git commands work exactly the same
|
|
46
|
+
gitpt status
|
|
47
|
+
gitpt log
|
|
48
|
+
gitpt branch
|
|
49
|
+
gitpt checkout -b new-feature
|
|
50
|
+
gitpt push origin main
|
|
51
|
+
|
|
52
|
+
# GitPT passes all arguments and options to git
|
|
53
|
+
gitpt log --oneline --graph
|
|
54
|
+
gitpt merge --no-ff feature-branch
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Adding Files
|
|
58
|
+
|
|
59
|
+
Add files to the staging area just like you would with git:
|
|
45
60
|
|
|
46
|
-
|
|
61
|
+
```bash
|
|
62
|
+
# Same as git add .
|
|
47
63
|
gitpt add .
|
|
64
|
+
|
|
65
|
+
# Same as git add -p
|
|
66
|
+
gitpt add -p
|
|
67
|
+
|
|
68
|
+
# Same as git add src/*.ts
|
|
69
|
+
gitpt add src/*.ts
|
|
48
70
|
```
|
|
49
71
|
|
|
50
72
|
### Creating Commits
|
|
@@ -62,7 +84,22 @@ The tool will:
|
|
|
62
84
|
4. Let you edit the message before committing
|
|
63
85
|
5. Create the commit with your approved message
|
|
64
86
|
|
|
65
|
-
###
|
|
87
|
+
### Changing Models
|
|
88
|
+
|
|
89
|
+
You can change the AI model at any time without re-entering your API key:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Select model interactively (fetches available models from OpenRouter)
|
|
93
|
+
gitpt model
|
|
94
|
+
|
|
95
|
+
# Specify model directly
|
|
96
|
+
gitpt model openai/gpt-4o
|
|
97
|
+
|
|
98
|
+
# Switch to a different Claude model
|
|
99
|
+
gitpt model anthropic/claude-3-haiku
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Commit Options
|
|
66
103
|
|
|
67
104
|
You can use any standard git commit options with the `gitpt commit` command:
|
|
68
105
|
|
|
@@ -77,9 +114,50 @@ gitpt commit -m "Your message here"
|
|
|
77
114
|
gitpt commit --amend
|
|
78
115
|
```
|
|
79
116
|
|
|
117
|
+
### Creating Pull Requests
|
|
118
|
+
|
|
119
|
+
Generate AI-powered pull request titles and descriptions based on your changes:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
gitpt pr create
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The tool will:
|
|
126
|
+
1. Analyze the commits and files changed since branching from the base branch
|
|
127
|
+
2. Generate a suitable PR title and detailed description
|
|
128
|
+
3. Show you the suggested content
|
|
129
|
+
4. Let you edit the title and description before submission
|
|
130
|
+
5. Create the pull request with your approved content
|
|
131
|
+
|
|
132
|
+
#### Pull Request Options
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# Create a draft PR
|
|
136
|
+
gitpt pr create --draft
|
|
137
|
+
|
|
138
|
+
# Specify a custom base branch
|
|
139
|
+
gitpt pr create --base develop
|
|
140
|
+
|
|
141
|
+
# Skip editing the PR details
|
|
142
|
+
gitpt pr create --no-edit
|
|
143
|
+
|
|
144
|
+
# Provide your own title instead of generating one
|
|
145
|
+
gitpt pr create --title "Your PR title here"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
> **Note:** This command requires GitHub CLI (`gh`) to be installed and authenticated.
|
|
149
|
+
|
|
80
150
|
## How It Works
|
|
81
151
|
|
|
82
|
-
GitPT
|
|
152
|
+
GitPT leverages AI via OpenRouter to enhance your Git workflow while acting as a complete git wrapper:
|
|
153
|
+
|
|
154
|
+
- **Command Handling:** GitPT intelligently routes commands - enhanced commands (commit, pr) use AI capabilities while all other git commands are passed directly to git.
|
|
155
|
+
|
|
156
|
+
- **For commits:** Sends a diff of your staged changes to the AI, which generates a contextual commit message following best practices.
|
|
157
|
+
|
|
158
|
+
- **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.
|
|
159
|
+
|
|
160
|
+
- **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
161
|
|
|
84
162
|
## Development
|
|
85
163
|
|
|
@@ -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.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import fetch from
|
|
2
|
-
import { getConfig } from
|
|
3
|
-
const OPENROUTER_API_URL =
|
|
1
|
+
import fetch from "node-fetch";
|
|
2
|
+
import { getConfig } from "./config.js";
|
|
3
|
+
const OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions";
|
|
4
4
|
export async function generateCommitMessage(diff) {
|
|
5
5
|
const config = getConfig();
|
|
6
6
|
if (!config) {
|
|
@@ -9,31 +9,34 @@ export async function generateCommitMessage(diff) {
|
|
|
9
9
|
const { apiKey, model } = config;
|
|
10
10
|
const messages = [
|
|
11
11
|
{
|
|
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
|
-
|
|
12
|
+
role: "system",
|
|
13
|
+
content: `You are a helpful assistant that generates concise, informative Git commit messages.
|
|
14
|
+
Follow these strict rules:
|
|
15
|
+
1. Use conventional commit format: type: description
|
|
16
|
+
2. Types are: feat, fix, docs, style, refactor, test, chore
|
|
17
|
+
3. NO scopes in parentheses - do not use feat(scope)
|
|
18
|
+
4. Keep the entire message under 100 characters
|
|
19
|
+
5. Use present tense (e.g., "add feature" not "added feature")
|
|
20
|
+
6. Be brief but descriptive about WHAT changed
|
|
21
|
+
7. Do not include detailed explanations
|
|
22
|
+
8. Examples:
|
|
23
|
+
- feat: add user authentication
|
|
24
|
+
- fix: resolve null pointer in login
|
|
25
|
+
- chore: update dependencies
|
|
26
|
+
- style: format css files`,
|
|
24
27
|
},
|
|
25
28
|
{
|
|
26
|
-
role:
|
|
27
|
-
content: `Generate a commit message for the following git diff:\n\n${diff}
|
|
28
|
-
}
|
|
29
|
+
role: "user",
|
|
30
|
+
content: `Generate a commit message for the following git diff:\n\n${diff}`,
|
|
31
|
+
},
|
|
29
32
|
];
|
|
30
33
|
try {
|
|
31
34
|
const response = await fetch(OPENROUTER_API_URL, {
|
|
32
|
-
method:
|
|
35
|
+
method: "POST",
|
|
33
36
|
headers: {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
Authorization: `Bearer ${apiKey}`,
|
|
39
|
+
"HTTP-Referer": "https://github.com/bartaxyz/GitPT",
|
|
37
40
|
},
|
|
38
41
|
body: JSON.stringify({
|
|
39
42
|
model: model,
|
|
@@ -45,11 +48,11 @@ export async function generateCommitMessage(diff) {
|
|
|
45
48
|
const errorText = await response.text();
|
|
46
49
|
throw new Error(`API request failed: ${response.status} ${response.statusText}\n${errorText}`);
|
|
47
50
|
}
|
|
48
|
-
const data = await response.json();
|
|
51
|
+
const data = (await response.json());
|
|
49
52
|
return data.choices[0].message.content.trim();
|
|
50
53
|
}
|
|
51
54
|
catch (error) {
|
|
52
|
-
console.error(
|
|
53
|
-
throw new Error(
|
|
55
|
+
console.error("Error generating commit message:", error);
|
|
56
|
+
throw new Error("Failed to generate commit message");
|
|
54
57
|
}
|
|
55
58
|
}
|