create-app-release 1.1.0 → 1.3.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/.prettierignore +1 -0
- package/CHANGELOG.md +25 -0
- package/README.md +56 -49
- package/package.json +2 -1
- package/src/index.js +316 -73
package/.prettierignore
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.3.0] - 2026-01-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added support for Google Gemini as an AI provider (API key and CLI integration).
|
|
13
|
+
- Introduced `--ai-provider` option to select between OpenAI, Gemini API, and Gemini CLI.
|
|
14
|
+
- Added `--gemini-key` and `--gemini-model` options for configuring Gemini API.
|
|
15
|
+
- Enabled direct integration with `gemini-cli` for summary generation.
|
|
16
|
+
|
|
17
|
+
## [1.2.0] - 2025-03-18
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Auto suggest repositories based on recent activities
|
|
22
|
+
- Added filter for listed pull requests for easy tracking
|
|
23
|
+
- Dynamic release version suggestion based on previous releases
|
|
24
|
+
- Added suggestion for source and target branches
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Updated release summary generation to exclude heading
|
|
29
|
+
|
|
8
30
|
## [1.1.0] - 2025-02-12
|
|
9
31
|
|
|
10
32
|
### Added
|
|
@@ -59,4 +81,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
59
81
|
- ora - Terminal spinners
|
|
60
82
|
- dotenv - Environment variable management
|
|
61
83
|
|
|
84
|
+
[1.3.0]: https://github.com/jamesgordo/create-app-release/releases/tag/v1.3.0
|
|
85
|
+
[1.2.0]: https://github.com/jamesgordo/create-app-release/releases/tag/v1.2.0
|
|
86
|
+
[1.1.0]: https://github.com/jamesgordo/create-app-release/releases/tag/v1.1.0
|
|
62
87
|
[1.0.0]: https://github.com/jamesgordo/create-app-release/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -7,25 +7,24 @@ An AI-powered GitHub release automation tool that helps you create release pull
|
|
|
7
7
|
|
|
8
8
|
## Features
|
|
9
9
|
|
|
10
|
-
- 🤖 AI-powered release notes generation
|
|
11
|
-
- 🔄 Flexible LLM
|
|
12
|
-
- OpenAI
|
|
13
|
-
-
|
|
14
|
-
- QwenAI
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
- 🌟 User-friendly CLI interface
|
|
10
|
+
- 🤖 AI-powered release notes generation.
|
|
11
|
+
- 🔄 **Flexible LLM Support**: Seamlessly switch between OpenAI, Google Gemini, and any OpenAI-compatible API.
|
|
12
|
+
- **OpenAI**: `gpt-4o`, `gpt-3.5-turbo`.
|
|
13
|
+
- **Google Gemini**: `gemini-pro` via API key or local `gemini-cli`.
|
|
14
|
+
- **OpenAI-Compatible**: Supports providers like Deepseek, QwenAI, or local LLMs via a custom base URL.
|
|
15
|
+
- 📦 Zero configuration - works right out of the box.
|
|
16
|
+
- 🔑 Secure token management through `git config`.
|
|
17
|
+
- 🎯 Interactive pull request selection.
|
|
18
|
+
- ✨ Professional markdown formatting.
|
|
19
|
+
- 📝 Smart categorization of changes.
|
|
20
|
+
- 🌟 User-friendly CLI interface.
|
|
22
21
|
|
|
23
22
|
## Prerequisites
|
|
24
23
|
|
|
25
24
|
- Node.js 14 or higher
|
|
26
25
|
- Git installed and configured
|
|
27
|
-
- GitHub account with repository access
|
|
28
|
-
-
|
|
26
|
+
- A GitHub account with repository access
|
|
27
|
+
- An account with an AI provider (e.g., OpenAI, Google Gemini) if using an API key.
|
|
29
28
|
|
|
30
29
|
## Usage
|
|
31
30
|
|
|
@@ -35,57 +34,65 @@ Run the tool directly using npx:
|
|
|
35
34
|
npx create-app-release
|
|
36
35
|
```
|
|
37
36
|
|
|
38
|
-
On first run, the tool will guide you through
|
|
39
|
-
|
|
40
|
-
1. Setting up your GitHub token (stored in git config)
|
|
41
|
-
2. Configuring your OpenAI API key (stored in git config)
|
|
42
|
-
3. Selecting pull requests for the release
|
|
43
|
-
4. Reviewing the AI-generated summary
|
|
44
|
-
5. Creating the release pull request
|
|
37
|
+
On the first run, the tool will guide you through setting up the necessary tokens and configurations.
|
|
45
38
|
|
|
46
39
|
### Token Setup
|
|
47
40
|
|
|
48
|
-
You
|
|
41
|
+
You will need a **GitHub Token** and an API key for your chosen AI provider.
|
|
49
42
|
|
|
50
|
-
1.
|
|
43
|
+
1. **GitHub Token** - Create at [GitHub Token Settings](https://github.com/settings/tokens/new)
|
|
44
|
+
- Required scope: `repo`
|
|
45
|
+
- Stored in git config as `github.token`
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
2. **OpenAI API Key** - Get from [OpenAI Platform](https://platform.openai.com/api-keys)
|
|
48
|
+
- Required if using the `openai` provider.
|
|
49
|
+
- Stored in git config as `openai.token`
|
|
54
50
|
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
3. **Gemini API Key** - Get from [Google AI Studio](https://makersuite.google.com/app/apikey)
|
|
52
|
+
- Required if using the `gemini` provider.
|
|
53
|
+
- Stored in git config as `gemini.token`
|
|
57
54
|
|
|
58
55
|
### Command-Line Options
|
|
59
56
|
|
|
60
|
-
|
|
57
|
+
#### General Options
|
|
61
58
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
`--ai-provider <provider>`
|
|
60
|
+
: Select the AI provider.
|
|
61
|
+
: **Options**: `openai`, `gemini`, `gemini-cli`.
|
|
62
|
+
: If not specified, you will be prompted to choose.
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
--openai-model <model>
|
|
68
|
-
# Examples: gpt-4o, gpt-3.5-turbo, deepseek-r1, qwen2.5
|
|
64
|
+
---
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
--openai-base-url <url>
|
|
72
|
-
# Examples:
|
|
73
|
-
# - Deepseek: https://api.deepseek.com/v1
|
|
74
|
-
# - QwenAI: https://api.qwen.ai/v1
|
|
75
|
-
# - Local: http://localhost:8000/v1
|
|
76
|
-
# - Custom: https://custom-openai-endpoint.com/v1
|
|
66
|
+
#### OpenAI Provider (`--ai-provider openai`)
|
|
77
67
|
|
|
78
|
-
|
|
68
|
+
`--openai-key <key>`
|
|
69
|
+
: Set your OpenAI API key directly.
|
|
79
70
|
|
|
80
|
-
|
|
81
|
-
|
|
71
|
+
`--openai-model <model>`
|
|
72
|
+
: Choose the OpenAI model (default: `"gpt-4o"`).
|
|
82
73
|
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
`--openai-base-url <url>`
|
|
75
|
+
: Set a custom base URL for OpenAI-compatible APIs (e.g., Deepseek, QwenAI, local LLMs).
|
|
76
|
+
: **Examples**:
|
|
77
|
+
: - `https://api.deepseek.com/v1`
|
|
78
|
+
: - `https://api.qwen.ai/v1`
|
|
79
|
+
: - `http://localhost:8000/v1`
|
|
85
80
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
#### Gemini Provider (`--ai-provider gemini`)
|
|
84
|
+
|
|
85
|
+
`--gemini-key <key>`
|
|
86
|
+
: Set your Gemini API key directly.
|
|
87
|
+
|
|
88
|
+
`--gemini-model <model>`
|
|
89
|
+
: Set the Gemini model to use (default: `"gemini-pro"`).
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
#### Gemini CLI Provider (`--ai-provider gemini-cli`)
|
|
94
|
+
|
|
95
|
+
This option uses a local `gemini` command-line tool, which must be installed and available in your system's `PATH`. The script will execute the `gemini` command, passing the prompt to its standard input. No API key is required for this provider option.
|
|
89
96
|
|
|
90
97
|
### Environment Variables (Optional)
|
|
91
98
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-app-release",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "AI-powered GitHub release automation tool",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
]
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
+
"@google/generative-ai": "^0.24.1",
|
|
48
49
|
"@octokit/rest": "^20.0.2",
|
|
49
50
|
"chalk": "^5.3.0",
|
|
50
51
|
"commander": "^11.1.0",
|
package/src/index.js
CHANGED
|
@@ -6,10 +6,14 @@ import { Octokit } from '@octokit/rest';
|
|
|
6
6
|
import inquirer from 'inquirer';
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import ora from 'ora';
|
|
9
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
9
10
|
import OpenAI from 'openai';
|
|
10
11
|
import { promisify } from 'util';
|
|
11
12
|
import { exec as execCallback } from 'child_process';
|
|
12
13
|
import { createRequire } from 'module';
|
|
14
|
+
import { tmpdir } from 'os';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import { writeFile, unlink } from 'fs/promises';
|
|
13
17
|
|
|
14
18
|
// Initialize utilities
|
|
15
19
|
const exec = promisify(execCallback);
|
|
@@ -98,12 +102,13 @@ async function configureToken({ envKey, gitKey, name, createUrl, additionalInfo
|
|
|
98
102
|
// Initialize API clients
|
|
99
103
|
let octokit;
|
|
100
104
|
let openai;
|
|
105
|
+
let gemini;
|
|
101
106
|
|
|
102
107
|
/**
|
|
103
|
-
* Initialize GitHub
|
|
104
|
-
* @returns {Promise<
|
|
108
|
+
* Initialize GitHub token
|
|
109
|
+
* @returns {Promise<string>} The GitHub token
|
|
105
110
|
*/
|
|
106
|
-
async function
|
|
111
|
+
async function initializeGitHubToken() {
|
|
107
112
|
const githubToken = await configureToken({
|
|
108
113
|
envKey: 'GITHUB_TOKEN',
|
|
109
114
|
gitKey: 'github.token',
|
|
@@ -111,15 +116,7 @@ async function initializeTokens() {
|
|
|
111
116
|
createUrl: 'https://github.com/settings/tokens/new',
|
|
112
117
|
additionalInfo: "Make sure to enable the 'repo' scope.",
|
|
113
118
|
});
|
|
114
|
-
|
|
115
|
-
const openaiToken = await configureToken({
|
|
116
|
-
envKey: 'OPENAI_API_KEY',
|
|
117
|
-
gitKey: 'openai.token',
|
|
118
|
-
name: 'OpenAI',
|
|
119
|
-
createUrl: 'https://platform.openai.com/api-keys',
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
return { githubToken, openaiToken };
|
|
119
|
+
return githubToken;
|
|
123
120
|
}
|
|
124
121
|
|
|
125
122
|
/**
|
|
@@ -170,14 +167,130 @@ function extractPRNumbersFromDescription(description) {
|
|
|
170
167
|
return new Set(prMatches.map((match) => parseInt(match.replace(/[^0-9]/g, ''))));
|
|
171
168
|
}
|
|
172
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Fetch repositories the user has contributed to, including personal and organization repos
|
|
172
|
+
* @returns {Promise<Array>} List of repositories
|
|
173
|
+
*/
|
|
174
|
+
async function fetchUserRepositories() {
|
|
175
|
+
const spinner = ora('Fetching your repositories...').start();
|
|
176
|
+
try {
|
|
177
|
+
// Get authenticated user info
|
|
178
|
+
const { data: user } = await octokit.rest.users.getAuthenticated();
|
|
179
|
+
const username = user.login;
|
|
180
|
+
|
|
181
|
+
// Get user's repositories (both owned and contributed to)
|
|
182
|
+
const repos = [];
|
|
183
|
+
|
|
184
|
+
// Fetch user's own repositories
|
|
185
|
+
const userReposIterator = octokit.paginate.iterator(
|
|
186
|
+
octokit.rest.repos.listForAuthenticatedUser,
|
|
187
|
+
{
|
|
188
|
+
per_page: 100,
|
|
189
|
+
sort: 'updated',
|
|
190
|
+
direction: 'desc',
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
for await (const { data: userRepos } of userReposIterator) {
|
|
195
|
+
repos.push(
|
|
196
|
+
...userRepos.map((repo) => ({
|
|
197
|
+
name: `${repo.owner.login}/${repo.name}`,
|
|
198
|
+
fullName: `${repo.owner.login}/${repo.name}`,
|
|
199
|
+
owner: repo.owner.login,
|
|
200
|
+
repoName: repo.name,
|
|
201
|
+
updatedAt: new Date(repo.updated_at),
|
|
202
|
+
pushedAt: new Date(repo.pushed_at || repo.updated_at),
|
|
203
|
+
isPersonal: repo.owner.login === username,
|
|
204
|
+
activityScore: 0, // Will be calculated based on user activity
|
|
205
|
+
}))
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Limit to 100 repositories to avoid excessive API calls
|
|
209
|
+
if (repos.length >= 100) break;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Get recent user activity for each repository (limited to top 15 to avoid API rate limits)
|
|
213
|
+
const topRepos = repos.slice(0, 15);
|
|
214
|
+
spinner.text = 'Analyzing your recent activity...';
|
|
215
|
+
|
|
216
|
+
// Process repositories in parallel with rate limiting
|
|
217
|
+
await Promise.all(
|
|
218
|
+
topRepos.map(async (repo, index) => {
|
|
219
|
+
// Add delay to avoid hitting rate limits
|
|
220
|
+
await new Promise((resolve) => setTimeout(resolve, index * 100));
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
// Check for user's recent commits
|
|
224
|
+
const { data: commits } = await octokit.rest.repos
|
|
225
|
+
.listCommits({
|
|
226
|
+
owner: repo.owner,
|
|
227
|
+
repo: repo.repoName,
|
|
228
|
+
author: username,
|
|
229
|
+
per_page: 100,
|
|
230
|
+
})
|
|
231
|
+
.catch(() => ({ data: [] }));
|
|
232
|
+
|
|
233
|
+
// Check for user's recent PRs
|
|
234
|
+
const { data: prs } = await octokit.rest.pulls
|
|
235
|
+
.list({
|
|
236
|
+
owner: repo.owner,
|
|
237
|
+
repo: repo.repoName,
|
|
238
|
+
state: 'all',
|
|
239
|
+
per_page: 100,
|
|
240
|
+
})
|
|
241
|
+
.catch(() => ({ data: [] }));
|
|
242
|
+
|
|
243
|
+
// Calculate activity score based on recency and count
|
|
244
|
+
const now = new Date();
|
|
245
|
+
let score = 0;
|
|
246
|
+
|
|
247
|
+
// Add points for recent commits
|
|
248
|
+
commits.forEach((commit) => {
|
|
249
|
+
const daysAgo = (now - new Date(commit.commit.author.date)) / (1000 * 60 * 60 * 24);
|
|
250
|
+
score += Math.max(30 - daysAgo, 0); // More points for more recent commits
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Add points for PRs authored or reviewed by user
|
|
254
|
+
prs.forEach((pr) => {
|
|
255
|
+
if (pr.user.login === username) {
|
|
256
|
+
const daysAgo = (now - new Date(pr.updated_at)) / (1000 * 60 * 60 * 24);
|
|
257
|
+
score += Math.max(20 - daysAgo, 0); // Points for authoring PRs
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Update the repository's activity score
|
|
262
|
+
repo.activityScore = score;
|
|
263
|
+
} catch (error) {
|
|
264
|
+
// Silently continue if we hit API limits or other issues
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// Sort repositories by activity score first, then by pushed date
|
|
270
|
+
repos.sort((a, b) => {
|
|
271
|
+
if (a.activityScore !== b.activityScore) {
|
|
272
|
+
return b.activityScore - a.activityScore; // Higher score first
|
|
273
|
+
}
|
|
274
|
+
return b.pushedAt - a.pushedAt; // Then by most recent push
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
spinner.succeed(`Found ${repos.length} repositories, sorted by your recent activity`);
|
|
278
|
+
return repos;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
spinner.fail('Failed to fetch repositories');
|
|
281
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
173
286
|
/**
|
|
174
287
|
* Fetch closed pull requests from the repository
|
|
175
288
|
* @param {string} owner - Repository owner
|
|
176
289
|
* @param {string} repo - Repository name
|
|
177
|
-
* @param {string}
|
|
290
|
+
* @param {string} baseBranch - Base branch name
|
|
178
291
|
* @returns {Promise<Array>} List of pull requests
|
|
179
292
|
*/
|
|
180
|
-
async function fetchPullRequests(owner, repo) {
|
|
293
|
+
async function fetchPullRequests(owner, repo, baseBranch) {
|
|
181
294
|
const spinner = ora('Fetching pull requests...').start();
|
|
182
295
|
try {
|
|
183
296
|
// Get the latest release PR first
|
|
@@ -195,10 +308,14 @@ async function fetchPullRequests(owner, repo) {
|
|
|
195
308
|
});
|
|
196
309
|
|
|
197
310
|
for await (const { data } of iterator) {
|
|
198
|
-
// Filter PRs that are merged after the last release
|
|
311
|
+
// Filter PRs that are merged after the last release, not included in it, and merged to the target branch
|
|
199
312
|
const relevantPRs = data.filter((pr) => {
|
|
313
|
+
// Skip PRs that aren't merged
|
|
200
314
|
if (!pr.merged_at) return false;
|
|
201
315
|
|
|
316
|
+
// Skip PRs that aren't targeting the specified branch
|
|
317
|
+
if (pr.base && pr.base.ref !== baseBranch) return false;
|
|
318
|
+
|
|
202
319
|
const isAfterLastRelease = latestReleasePR
|
|
203
320
|
? new Date(pr.merged_at) >= new Date(latestReleasePR.merged_at)
|
|
204
321
|
: true;
|
|
@@ -229,9 +346,10 @@ async function fetchPullRequests(owner, repo) {
|
|
|
229
346
|
/**
|
|
230
347
|
* Generate an AI-powered release summary from selected pull requests
|
|
231
348
|
* @param {Array} selectedPRs - List of selected pull requests
|
|
349
|
+
* @param {string} aiProvider - The AI provider to use ('openai' or 'gemini')
|
|
232
350
|
* @returns {Promise<string>} Generated release summary
|
|
233
351
|
*/
|
|
234
|
-
async function generateSummary(selectedPRs) {
|
|
352
|
+
async function generateSummary(selectedPRs, aiProvider) {
|
|
235
353
|
const spinner = ora('Generating release summary...').start();
|
|
236
354
|
try {
|
|
237
355
|
const prDetails = selectedPRs.map((pr) => ({
|
|
@@ -250,28 +368,46 @@ async function generateSummary(selectedPRs) {
|
|
|
250
368
|
1.2 For each type, make each bullet point concise and easy to read and understand for non-tech people.
|
|
251
369
|
1.3 Don't link the bullet points to a pull requests
|
|
252
370
|
2. The last section should be a list of pull requests included in the release. Format: "#<number> - <title> by [@<author>](<authorUrl>) (<date>)".
|
|
371
|
+
3. Don't add Release Summary title/heading.
|
|
253
372
|
|
|
254
373
|
Pull Requests to summarize:
|
|
255
374
|
${JSON.stringify(prDetails, null, 2)}
|
|
256
375
|
|
|
257
376
|
Keep the summary concise, clear, and focused on the user impact. Use professional but easy-to-understand language.`;
|
|
258
377
|
|
|
259
|
-
|
|
260
|
-
const response = await openai.chat.completions.create({
|
|
261
|
-
model,
|
|
262
|
-
messages: [{ role: 'user', content: prompt }],
|
|
263
|
-
temperature: 0.7,
|
|
264
|
-
});
|
|
378
|
+
let summaryText;
|
|
265
379
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
380
|
+
if (aiProvider === 'openai') {
|
|
381
|
+
const model = program.opts().openaiModel || 'gpt-4o';
|
|
382
|
+
const response = await openai.chat.completions.create({
|
|
383
|
+
model,
|
|
384
|
+
messages: [{ role: 'user', content: prompt }],
|
|
385
|
+
temperature: 0.7,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
if (!response?.choices?.length || !response.choices[0]?.message?.content) {
|
|
389
|
+
throw new Error('Invalid API response structure from OpenAI.');
|
|
390
|
+
}
|
|
391
|
+
summaryText = response.choices[0].message.content;
|
|
392
|
+
} else if (aiProvider === 'gemini') {
|
|
393
|
+
const modelName = program.opts().geminiModel || 'gemini-pro';
|
|
394
|
+
const model = gemini.getGenerativeModel({ model: modelName });
|
|
395
|
+
const result = await model.generateContent(prompt);
|
|
396
|
+
const response = await result.response;
|
|
397
|
+
summaryText = response.text();
|
|
398
|
+
} else if (aiProvider === 'gemini-cli') {
|
|
399
|
+
const tempFilePath = join(tmpdir(), `gemini-prompt-${Date.now()}.txt`);
|
|
400
|
+
try {
|
|
401
|
+
await writeFile(tempFilePath, prompt, 'utf-8');
|
|
402
|
+
const { stdout } = await exec(`gemini < ${tempFilePath}`);
|
|
403
|
+
summaryText = stdout;
|
|
404
|
+
} finally {
|
|
405
|
+
await unlink(tempFilePath);
|
|
406
|
+
}
|
|
271
407
|
}
|
|
272
408
|
|
|
273
409
|
spinner.succeed('Summary generated successfully');
|
|
274
|
-
return
|
|
410
|
+
return summaryText;
|
|
275
411
|
} catch (error) {
|
|
276
412
|
spinner.fail('Failed to generate summary');
|
|
277
413
|
|
|
@@ -362,41 +498,152 @@ async function run() {
|
|
|
362
498
|
const options = program.opts();
|
|
363
499
|
|
|
364
500
|
// Initialize GitHub token
|
|
365
|
-
const
|
|
501
|
+
const githubToken = await initializeGitHubToken();
|
|
502
|
+
octokit = new Octokit({ auth: githubToken });
|
|
366
503
|
|
|
367
|
-
//
|
|
368
|
-
let
|
|
369
|
-
if (!
|
|
370
|
-
const
|
|
371
|
-
|
|
504
|
+
// AI Provider selection
|
|
505
|
+
let aiProvider = options.aiProvider;
|
|
506
|
+
if (!aiProvider) {
|
|
507
|
+
const { provider } = await inquirer.prompt([
|
|
508
|
+
{
|
|
509
|
+
type: 'list',
|
|
510
|
+
name: 'provider',
|
|
511
|
+
message: 'Select an AI provider for generating summaries:',
|
|
512
|
+
choices: [
|
|
513
|
+
{ name: 'OpenAI', value: 'openai' },
|
|
514
|
+
{ name: 'Gemini (API Key)', value: 'gemini' },
|
|
515
|
+
{ name: 'Gemini (CLI)', value: 'gemini-cli' },
|
|
516
|
+
],
|
|
517
|
+
default: 'openai',
|
|
518
|
+
},
|
|
519
|
+
]);
|
|
520
|
+
aiProvider = provider;
|
|
372
521
|
}
|
|
373
522
|
|
|
374
|
-
// Initialize
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
523
|
+
// Initialize AI client
|
|
524
|
+
if (aiProvider === 'openai') {
|
|
525
|
+
const openaiToken =
|
|
526
|
+
options.openaiKey ||
|
|
527
|
+
(await configureToken({
|
|
528
|
+
envKey: 'OPENAI_API_KEY',
|
|
529
|
+
gitKey: 'openai.token',
|
|
530
|
+
name: 'OpenAI',
|
|
531
|
+
createUrl: 'https://platform.openai.com/api-keys',
|
|
532
|
+
}));
|
|
533
|
+
openai = new OpenAI({
|
|
534
|
+
apiKey: openaiToken,
|
|
535
|
+
baseURL: options.openaiBaseUrl,
|
|
536
|
+
});
|
|
537
|
+
} else if (aiProvider === 'gemini') {
|
|
538
|
+
const geminiToken =
|
|
539
|
+
options.geminiKey ||
|
|
540
|
+
(await configureToken({
|
|
541
|
+
envKey: 'GEMINI_API_KEY',
|
|
542
|
+
gitKey: 'gemini.token',
|
|
543
|
+
name: 'Gemini',
|
|
544
|
+
createUrl: 'https://makersuite.google.com/app/apikey',
|
|
545
|
+
}));
|
|
546
|
+
gemini = new GoogleGenerativeAI(geminiToken);
|
|
547
|
+
} else if (aiProvider === 'gemini-cli') {
|
|
548
|
+
// No initialization needed, but I can check if `gemini` command exists
|
|
549
|
+
try {
|
|
550
|
+
await exec('command -v gemini');
|
|
551
|
+
} catch (error) {
|
|
552
|
+
console.error(
|
|
553
|
+
chalk.red(
|
|
554
|
+
'Error: `gemini` command not found. Please install the gemini-cli tool and make sure it is in your PATH.'
|
|
555
|
+
)
|
|
556
|
+
);
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
378
560
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
561
|
+
// Fetch repositories the user has contributed to
|
|
562
|
+
const userRepos = await fetchUserRepositories();
|
|
563
|
+
|
|
564
|
+
// Prepare repository choices
|
|
565
|
+
const repoChoices =
|
|
566
|
+
userRepos.length > 0
|
|
567
|
+
? userRepos.map((repo) => ({
|
|
568
|
+
name: repo.fullName + (repo.isPersonal ? ' (personal)' : ''),
|
|
569
|
+
value: { owner: repo.owner, repo: repo.repoName },
|
|
570
|
+
}))
|
|
571
|
+
: [];
|
|
572
|
+
|
|
573
|
+
// Add option for manual entry
|
|
574
|
+
repoChoices.push({ name: '-- Enter repository manually --', value: 'manual' });
|
|
575
|
+
|
|
576
|
+
let repoInfo = { owner: '', repo: '' };
|
|
577
|
+
const { repoSelection } = await inquirer.prompt([
|
|
578
|
+
{
|
|
579
|
+
type: 'list',
|
|
580
|
+
name: 'repoSelection',
|
|
581
|
+
message: 'Select a repository:',
|
|
582
|
+
choices: repoChoices,
|
|
583
|
+
pageSize: 5,
|
|
584
|
+
},
|
|
585
|
+
]);
|
|
586
|
+
|
|
587
|
+
// Handle manual repository entry
|
|
588
|
+
if (repoSelection === 'manual') {
|
|
589
|
+
const manualEntry = await inquirer.prompt([
|
|
590
|
+
{
|
|
591
|
+
type: 'input',
|
|
592
|
+
name: 'owner',
|
|
593
|
+
message: 'Enter repository owner:',
|
|
594
|
+
validate: (input) => input.length > 0,
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
type: 'input',
|
|
598
|
+
name: 'repo',
|
|
599
|
+
message: 'Enter repository name:',
|
|
600
|
+
validate: (input) => input.length > 0,
|
|
601
|
+
},
|
|
602
|
+
]);
|
|
603
|
+
repoInfo = manualEntry;
|
|
604
|
+
} else {
|
|
605
|
+
repoInfo = repoSelection;
|
|
606
|
+
}
|
|
383
607
|
|
|
384
|
-
const { owner, repo } =
|
|
608
|
+
const { owner, repo } = repoInfo;
|
|
609
|
+
|
|
610
|
+
const { sourceBranch, targetBranch } = await inquirer.prompt([
|
|
385
611
|
{
|
|
386
612
|
type: 'input',
|
|
387
|
-
name: '
|
|
388
|
-
message: 'Enter
|
|
613
|
+
name: 'sourceBranch',
|
|
614
|
+
message: 'Enter source branch name:',
|
|
615
|
+
default: 'staging',
|
|
389
616
|
validate: (input) => input.length > 0,
|
|
390
617
|
},
|
|
391
618
|
{
|
|
392
619
|
type: 'input',
|
|
393
|
-
name: '
|
|
394
|
-
message: 'Enter
|
|
620
|
+
name: 'targetBranch',
|
|
621
|
+
message: 'Enter target branch name:',
|
|
622
|
+
default: 'main',
|
|
395
623
|
validate: (input) => input.length > 0,
|
|
396
624
|
},
|
|
397
625
|
]);
|
|
398
626
|
|
|
399
|
-
|
|
627
|
+
// Get the latest release version for the repository
|
|
628
|
+
let suggestedVersion = '1.0.0';
|
|
629
|
+
try {
|
|
630
|
+
const { data: releases } = await octokit.rest.repos.listReleases({
|
|
631
|
+
owner,
|
|
632
|
+
repo,
|
|
633
|
+
per_page: 100,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
if (releases && releases.length > 0) {
|
|
637
|
+
// Extract version from tag_name (removing any 'v' prefix)
|
|
638
|
+
const latestTag = releases[0].tag_name.replace(/^v/, '');
|
|
639
|
+
const [major, minor, patch] = latestTag.split('.');
|
|
640
|
+
suggestedVersion = `${major}.${minor}.${parseInt(patch) + 1}`;
|
|
641
|
+
}
|
|
642
|
+
} catch (error) {
|
|
643
|
+
console.log(chalk.yellow(`Could not fetch latest release version: ${error.message}`));
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const pulls = await fetchPullRequests(owner, repo, sourceBranch);
|
|
400
647
|
|
|
401
648
|
const { selectedPRs } = await inquirer.prompt([
|
|
402
649
|
{
|
|
@@ -425,7 +672,7 @@ async function run() {
|
|
|
425
672
|
|
|
426
673
|
let summary;
|
|
427
674
|
if (summaryType === 'ai') {
|
|
428
|
-
summary = await generateSummary(selectedPRs);
|
|
675
|
+
summary = await generateSummary(selectedPRs, aiProvider);
|
|
429
676
|
} else {
|
|
430
677
|
summary = selectedPRs
|
|
431
678
|
.map((pr) => {
|
|
@@ -438,11 +685,12 @@ async function run() {
|
|
|
438
685
|
console.log(chalk.cyan('\nSummary:'));
|
|
439
686
|
console.log(summary);
|
|
440
687
|
|
|
441
|
-
const { version, confirm
|
|
688
|
+
const { version, confirm } = await inquirer.prompt([
|
|
442
689
|
{
|
|
443
690
|
type: 'input',
|
|
444
691
|
name: 'version',
|
|
445
|
-
message:
|
|
692
|
+
message: `Enter the version number for this release (suggested: ${suggestedVersion}):`,
|
|
693
|
+
default: suggestedVersion,
|
|
446
694
|
validate: (input) => {
|
|
447
695
|
// Validate semantic versioning format (x.y.z)
|
|
448
696
|
const semverRegex = /^\d+\.\d+\.\d+$/;
|
|
@@ -457,20 +705,6 @@ async function run() {
|
|
|
457
705
|
name: 'confirm',
|
|
458
706
|
message: 'Would you like to create a release PR with this summary?',
|
|
459
707
|
},
|
|
460
|
-
{
|
|
461
|
-
type: 'input',
|
|
462
|
-
name: 'sourceBranch',
|
|
463
|
-
message: 'Enter source branch name:',
|
|
464
|
-
when: (answers) => answers.confirm,
|
|
465
|
-
validate: (input) => input.length > 0,
|
|
466
|
-
},
|
|
467
|
-
{
|
|
468
|
-
type: 'input',
|
|
469
|
-
name: 'targetBranch',
|
|
470
|
-
message: 'Enter target branch name:',
|
|
471
|
-
when: (answers) => answers.confirm,
|
|
472
|
-
validate: (input) => input.length > 0,
|
|
473
|
-
},
|
|
474
708
|
]);
|
|
475
709
|
|
|
476
710
|
if (confirm) {
|
|
@@ -490,27 +724,36 @@ async function run() {
|
|
|
490
724
|
const description = `AI-powered GitHub release automation tool
|
|
491
725
|
|
|
492
726
|
Options:
|
|
493
|
-
--
|
|
494
|
-
--openai-
|
|
495
|
-
|
|
727
|
+
--ai-provider <provider> Set AI provider to use ('openai', 'gemini', or 'gemini-cli')
|
|
728
|
+
--openai-key <key> Set OpenAI API key directly
|
|
729
|
+
--openai-model <model> Set OpenAI model to use (default: "gpt-4o")
|
|
496
730
|
--openai-base-url <url> Set custom OpenAI API base URL
|
|
497
|
-
|
|
731
|
+
--gemini-key <key> Set Gemini API key directly (for 'gemini' provider)
|
|
732
|
+
--gemini-model <model> Set Gemini model to use (default: "gemini-pro")
|
|
498
733
|
|
|
499
734
|
Environment Variables:
|
|
500
735
|
GITHUB_TOKEN GitHub personal access token
|
|
501
|
-
OPENAI_API_KEY OpenAI API key
|
|
736
|
+
OPENAI_API_KEY OpenAI API key
|
|
737
|
+
GEMINI_API_KEY Gemini API key
|
|
502
738
|
|
|
503
739
|
Git Config:
|
|
504
740
|
github.token GitHub token in git config
|
|
505
|
-
openai.token OpenAI token in git config
|
|
741
|
+
openai.token OpenAI token in git config
|
|
742
|
+
gemini.token Gemini token in git config
|
|
506
743
|
`;
|
|
507
744
|
|
|
508
745
|
program
|
|
509
746
|
.name('create-app-release')
|
|
510
747
|
.description(description)
|
|
511
748
|
.version(pkg.version)
|
|
749
|
+
.option(
|
|
750
|
+
'--ai-provider <provider>',
|
|
751
|
+
"Set AI provider to use ('openai', 'gemini', or 'gemini-cli')"
|
|
752
|
+
)
|
|
512
753
|
.option('--openai-base-url <url>', 'Set custom OpenAI API base URL')
|
|
513
|
-
.option('--openai-model <model>', 'Set OpenAI model to use (default: "gpt-
|
|
514
|
-
.option('--openai-key <key>', 'Set OpenAI API key directly
|
|
754
|
+
.option('--openai-model <model>', 'Set OpenAI model to use (default: "gpt-4o")')
|
|
755
|
+
.option('--openai-key <key>', 'Set OpenAI API key directly')
|
|
756
|
+
.option('--gemini-model <model>', 'Set Gemini model to use (default: "gemini-pro")')
|
|
757
|
+
.option('--gemini-key <key>', "Set Gemini API key directly (for 'gemini' provider)")
|
|
515
758
|
.action(run)
|
|
516
759
|
.parse(process.argv);
|