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 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
- - Compatible with all regular git commit options (you can treat it as an alias)
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
- ### Adding Files
40
+ ### Using GitPT as a Complete Git Replacement
39
41
 
40
- Add files to the staging area using either git directly or the GitPT wrapper:
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 command
44
- git add .
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
- # Or using GitPT wrapper
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
- ### Options
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 sends a diff of your staged changes to the configured AI model via OpenRouter, which generates a contextual commit message following best practices.
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,10 @@
1
+ interface PullRequestOptions {
2
+ title?: string;
3
+ body?: string;
4
+ draft?: boolean;
5
+ base?: string;
6
+ edit?: boolean;
7
+ [key: string]: any;
8
+ }
9
+ export declare function prCreateCommand(options?: PullRequestOptions): Promise<void>;
10
+ export {};
@@ -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
- // Setup command
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
- // Add command (pass-through to git add)
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 'node-fetch';
2
- import { getConfig } from './config.js';
3
- const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
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: 'system',
13
- content: `You are a helpful assistant that generates concise, informative Git commit messages.
14
- Follow conventional commit style. Be brief but descriptive.
15
- Focus on WHAT changes were made, WHY they were made, and their IMPACT.
16
- Use present tense (e.g., "add feature" not "added feature").
17
- Split the message like this if needed:
18
-
19
- feat(scope): short description
20
-
21
- More detailed explanation if necessary
22
-
23
- Only include a detailed explanation for complex changes.`
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: 'user',
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: 'POST',
35
+ method: "POST",
33
36
  headers: {
34
- 'Content-Type': 'application/json',
35
- 'Authorization': `Bearer ${apiKey}`,
36
- 'HTTP-Referer': 'https://github.com/bartaxyz/GitPT',
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('Error generating commit message:', error);
53
- throw new Error('Failed to generate commit message');
55
+ console.error("Error generating commit message:", error);
56
+ throw new Error("Failed to generate commit message");
54
57
  }
55
58
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitpt",
3
- "version": "1.0.0",
3
+ "version": "1.1.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",