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 CHANGED
@@ -4,3 +4,4 @@ build
4
4
  coverage
5
5
  .next
6
6
  package-lock.json
7
+ pnpm-lock.yaml
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 using GPT-4
11
- - 🔄 Flexible LLM support:
12
- - OpenAI models (GPT-4o, GPT-3.5-turbo)
13
- - Deepseek models
14
- - QwenAI models
15
- - Local LLM deployments
16
- - 📦 Zero configuration - works right out of the box
17
- - 🔑 Secure token management through git config
18
- - 🎯 Interactive pull request selection
19
- - Professional markdown formatting
20
- - 📝 Smart categorization of changes
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
- - OpenAI account (for GPT-4 access)
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'll need two tokens to use this tool:
41
+ You will need a **GitHub Token** and an API key for your chosen AI provider.
49
42
 
50
- 1. **GitHub Token** - Create at [GitHub Token Settings](https://github.com/settings/tokens/new)
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
- - Required scope: `repo`
53
- - Will be stored in git config as `github.token`
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
- 2. **OpenAI API Key** - Get from [OpenAI Platform](https://platform.openai.com/api-keys)
56
- - Will be stored in git config as `openai.token`
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
- Customize the tool's behavior using these command-line options:
57
+ #### General Options
61
58
 
62
- ```bash
63
- # Set OpenAI API key directly (alternative to env/git config)
64
- --openai-key <key>
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
- # Choose OpenAI model (default: "gpt-4o")
67
- --openai-model <model>
68
- # Examples: gpt-4o, gpt-3.5-turbo, deepseek-r1, qwen2.5
64
+ ---
69
65
 
70
- # Set custom OpenAI API base URL
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
- # Full example with different providers:
68
+ `--openai-key <key>`
69
+ : Set your OpenAI API key directly.
79
70
 
80
- # Using Deepseek
81
- npx create-app-release --openai-base-url https://api.deepseek.com/v1 --openai-key your_deepseek_key --openai-model deepseek-chat
71
+ `--openai-model <model>`
72
+ : Choose the OpenAI model (default: `"gpt-4o"`).
82
73
 
83
- # Using QwenAI
84
- npx create-app-release --openai-base-url https://api.qwen.ai/v1 --openai-key your_qwen_key --openai-model qwen-14b-chat
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
- # Using Local LLM
87
- npx create-app-release --openai-base-url http://localhost:8000/v1 --openai-model local-model
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.1.0",
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 and OpenAI tokens
104
- * @returns {Promise<Object>} Object containing both tokens
108
+ * Initialize GitHub token
109
+ * @returns {Promise<string>} The GitHub token
105
110
  */
106
- async function initializeTokens() {
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} targetBranch - Target branch name
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 and not included in it
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
- const model = program.opts().openaiModel || 'gpt-4o';
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
- // Validate response structure
267
- if (!response?.choices?.length || !response.choices[0]?.message?.content) {
268
- throw new Error(
269
- 'Invalid API response structure. Expected response.choices[0].message.content'
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 response.choices[0].message.content;
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 { githubToken } = await initializeTokens();
501
+ const githubToken = await initializeGitHubToken();
502
+ octokit = new Octokit({ auth: githubToken });
366
503
 
367
- // Get OpenAI token from command line or fallback to configuration
368
- let openaiToken = options.openaiKey;
369
- if (!openaiToken) {
370
- const tokens = await initializeTokens();
371
- openaiToken = tokens.openaiToken;
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 clients with tokens
375
- octokit = new Octokit({
376
- auth: githubToken,
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
- openai = new OpenAI({
380
- apiKey: openaiToken,
381
- baseURL: options.openaiBaseUrl,
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 } = await inquirer.prompt([
608
+ const { owner, repo } = repoInfo;
609
+
610
+ const { sourceBranch, targetBranch } = await inquirer.prompt([
385
611
  {
386
612
  type: 'input',
387
- name: 'owner',
388
- message: 'Enter repository owner:',
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: 'repo',
394
- message: 'Enter repository name:',
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
- const pulls = await fetchPullRequests(owner, repo);
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, sourceBranch, targetBranch } = await inquirer.prompt([
688
+ const { version, confirm } = await inquirer.prompt([
442
689
  {
443
690
  type: 'input',
444
691
  name: 'version',
445
- message: 'Enter the version number for this release (e.g., 1.2.3):',
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
- --openai-key <key> Set OpenAI API key directly (alternative to env/git config)
494
- --openai-model <model> Set OpenAI model to use (default: "gpt-4")
495
- Examples: gpt-4, gpt-3.5-turbo
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
- Example: https://custom-openai-endpoint.com/v1
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 (if not using --openai-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 (if not using --openai-key)
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-4")')
514
- .option('--openai-key <key>', 'Set OpenAI API key directly (alternative to env/git config)')
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);