ai-git-tools 1.0.4 → 2.0.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.
@@ -1,303 +1,187 @@
1
1
  /**
2
2
  * PR 命令
3
- *
4
- * 生成 PR 標題、描述並創建 Pull Request
3
+ * 基于 scripts/ai-auto-pr.mjs 简化版
4
+ * 生成 PR 标题、描述并创建 Pull Request
5
5
  */
6
6
 
7
- import chalk from 'chalk';
8
- import inquirer from 'inquirer';
9
- import { loadConfig } from '../core/config-loader.js';
10
- import { AIClient } from '../core/ai-client.js';
7
+ import { execSync } from 'child_process';
8
+ import { loadPRConfig } from '../core/config-loader.js';
11
9
  import { GitOperations } from '../core/git-operations.js';
12
- import { GitHubAPI } from '../core/github-api.js';
10
+ import { AIClient } from '../core/ai-client.js';
13
11
  import { Logger } from '../utils/logger.js';
14
- import {
15
- handleError,
16
- truncateText,
17
- getProjectTypePrompt,
18
- } from '../utils/helpers.js';
19
-
20
- /**
21
- * 決定 base 分支
22
- */
23
- async function determineBaseBranch(config, logger) {
24
- if (config.baseBranch && config.baseBranch !== 'auto') {
25
- return config.baseBranch;
26
- }
27
-
28
- // 嘗試找最新的 release 分支
29
- logger.startSpinner('尋找目標分支...');
30
- const latestRelease = GitHubAPI.getLatestReleaseBranch();
31
-
32
- if (latestRelease) {
33
- logger.succeedSpinner(`自動選擇目標分支: ${latestRelease}`);
34
- return latestRelease;
35
- }
36
-
37
- // 預設使用 main 或 master
38
- const defaultBranch = GitOperations.branchExists('main') ? 'main' : 'master';
39
- logger.succeedSpinner(`使用預設分支: ${defaultBranch}`);
40
- return defaultBranch;
41
- }
12
+ import { handleError, getProjectTypePrompt } from '../utils/helpers.js';
42
13
 
43
14
  /**
44
- * 使用 AI 生成 PR 內容
15
+ * 生成 PR 内容
45
16
  */
46
- async function generatePRContent(baseBranch, headBranch, config, logger) {
47
- logger.startSpinner('AI 正在分析變更並生成 PR 內容...');
48
-
49
- // 獲取 diff 和 commits
50
- const diff = GitOperations.getDiffBetweenBranches(baseBranch, headBranch);
51
- const commits = GitOperations.getCommitsBetweenBranches(baseBranch, headBranch);
17
+ async function generatePRContent(baseBranch, headBranch, config) {
18
+ const logger = new Logger();
19
+ logger.step('AI 正在分析变更并生成 PR 内容...');
52
20
 
53
- const truncatedDiff = truncateText(diff, config.ai.maxDiffLength);
21
+ // 获取 diff 和 commits
22
+ const diff = GitOperations.getDiff(baseBranch, headBranch);
23
+ const commits = GitOperations.getCommits(baseBranch, headBranch);
54
24
 
55
- const aiClient = new AIClient(config);
25
+ // 智能截断 diff
26
+ const truncatedDiff = GitOperations.truncateDiff(diff, config.ai.maxDiffLength);
56
27
 
57
28
  const prompt = `${getProjectTypePrompt()}
58
29
 
59
- 請根據以下資訊生成一個 Pull Request 的標題和描述。
30
+ 请根据以下资讯生成一个 Pull Request 的标题和描述。
60
31
 
61
32
  **Commits 列表**:
62
- ${commits.join('\n')}
33
+ ${commits}
63
34
 
64
35
  **Git Diff**:
65
36
  ${truncatedDiff}
66
37
 
67
- **輸出格式**(JSON):
38
+ **输出格式**(JSON):
68
39
  {
69
- "title": "PR 標題(簡短、繁體中文,50 字內)",
70
- "description": "PR 描述(使用 Markdown 格式,繁體中文)"
40
+ "title": "PR 标题(简短、繁体中文,50 字内)",
41
+ "description": "PR 描述(使用 Markdown 格式,繁体中文)"
71
42
  }
72
43
 
73
- **PR 描述應包含**:
74
- 1. ## 📝 變更摘要(簡述主要變更)
75
- 2. ## ✨ 主要功能(列出新增功能或修正項目)
76
- 3. ## 🔧 技術細節(選填,如有重要的技術變更)
77
- 4. ## ✅ 測試(如何測試這些變更)
44
+ **PR 描述应包含**:
45
+ 1. ## 📝 变更摘要(简述主要变更)
46
+ 2. ## ✨ 主要功能(列出新增功能或修正项目)
47
+ 3. ## 🔧 技术细节(选填,如有重要的技术变更)
48
+ 4. ## ✅ 测试(如何测试这些变更)
78
49
 
79
- 請只輸出 JSON,不要其他文字。`;
50
+ 请只输出 JSON,不要其他文字。`;
80
51
 
81
- const response = await aiClient.sendAndWait(prompt);
82
- await aiClient.stop();
52
+ const response = await AIClient.sendAndWait(prompt, config.ai.model);
83
53
 
84
54
  try {
85
55
  const prContent = AIClient.parseJSON(response);
86
- logger.succeedSpinner('PR 內容生成完成');
56
+ logger.success('PR 内容生成完成\n');
87
57
  return prContent;
88
58
  } catch (error) {
89
- logger.failSpinner('生成 PR 內容失敗');
90
- throw new Error(`無法解析 AI 回應: ${error.message}`);
59
+ throw new Error(`无法解析 AI 回应: ${error.message}`);
91
60
  }
92
61
  }
93
62
 
94
63
  /**
95
- * 選擇 Reviewers
64
+ * 显示 PR 预览
96
65
  */
97
- async function selectReviewers(changedFiles, config, logger) {
98
- if (!config.reviewers.autoSelect) {
99
- return [];
100
- }
101
-
102
- logger.startSpinner('分析潛在的 reviewers...');
103
-
104
- const contributorsMap = new Map();
105
-
106
- // 分析每個變更檔案的貢獻者
107
- for (const file of changedFiles.slice(0, 10)) { // 限制分析前 10 個檔案
108
- const contributors = GitOperations.getFileContributors(
109
- file,
110
- config.reviewers.gitHistoryDepth
111
- );
112
-
113
- for (const { email, name } of contributors) {
114
- // 排除設定中的作者
115
- if (config.reviewers.excludeAuthors.some(pattern => email.includes(pattern))) {
116
- continue;
117
- }
118
-
119
- const count = contributorsMap.get(email) || 0;
120
- contributorsMap.set(email, count + 1);
121
- }
122
- }
123
-
124
- // 排序並取前 N 位
125
- const suggested = Array.from(contributorsMap.entries())
126
- .sort((a, b) => b[1] - a[1])
127
- .slice(0, config.reviewers.maxSuggested)
128
- .map(([email]) => email);
129
-
130
- logger.succeedSpinner(`找到 ${suggested.length} 位潛在 reviewers`);
131
-
132
- if (suggested.length === 0) {
133
- return [];
134
- }
135
-
136
- // 互動式選擇
137
- const { selectedReviewers } = await inquirer.prompt([
138
- {
139
- type: 'checkbox',
140
- name: 'selectedReviewers',
141
- message: '選擇 Reviewers:',
142
- choices: suggested.map(email => ({
143
- name: email,
144
- value: email,
145
- })),
146
- },
147
- ]);
148
-
149
- return selectedReviewers;
66
+ function displayPreview(prContent, stats) {
67
+ console.log('\n' + '═'.repeat(60));
68
+ console.log('📋 PR 预览');
69
+ console.log('═'.repeat(60));
70
+ console.log(`\n标题: ${prContent.title}\n`);
71
+ console.log(`统计: ${stats.stats}`);
72
+ console.log(`文件数: ${stats.filesChanged} 个文件\n`);
73
+ console.log('─'.repeat(60));
74
+ console.log('描述:\n');
75
+ console.log(prContent.description);
76
+ console.log('\n' + '═'.repeat(60) + '\n');
150
77
  }
151
78
 
152
79
  /**
153
- * 智能選擇 Labels
80
+ * PR 命令主函数
154
81
  */
155
- function suggestLabels(prContent, availableLabels) {
156
- const suggestions = [];
157
- const title = prContent.title.toLowerCase();
158
- const description = prContent.description.toLowerCase();
159
-
160
- // 根據關鍵字建議 labels
161
- const labelKeywords = {
162
- bug: ['fix', 'bug', '修正', '錯誤'],
163
- enhancement: ['feat', 'feature', '新增', '功能'],
164
- documentation: ['docs', '文件', 'readme'],
165
- refactor: ['refactor', '重構'],
166
- performance: ['perf', 'performance', '效能', '優化'],
167
- test: ['test', '測試'],
168
- ui: ['ui', 'style', '樣式', '介面'],
169
- api: ['api', 'endpoint'],
170
- };
171
-
172
- for (const [label, keywords] of Object.entries(labelKeywords)) {
173
- if (availableLabels.includes(label)) {
174
- if (keywords.some(kw => title.includes(kw) || description.includes(kw))) {
175
- suggestions.push(label);
176
- }
177
- }
178
- }
179
-
180
- return suggestions;
181
- }
182
-
183
- /**
184
- * PR 命令處理器
185
- */
186
- export async function prCommand(options) {
187
- const logger = new Logger(options.verbose);
82
+ export async function prCommand() {
83
+ const logger = new Logger();
188
84
 
189
85
  try {
190
86
  logger.header('AI Auto PR Generator');
191
87
 
192
- // 檢查環境
193
- if (!GitOperations.isGitRepository()) {
194
- logger.error('當前目錄不是 Git 倉庫');
195
- process.exit(1);
88
+ // 载入配置
89
+ const config = await loadPRConfig();
90
+
91
+ // 获取当前分支
92
+ const currentBranch = GitOperations.getCurrentBranch();
93
+ let headBranch = config.headBranch || currentBranch;
94
+ let baseBranch = config.baseBranch;
95
+
96
+ // 自动侦测 base branch
97
+ if (!baseBranch) {
98
+ logger.step('正在侦测 release 分支...\n');
99
+ const latestRelease = GitOperations.findLatestReleaseBranch();
100
+
101
+ if (latestRelease) {
102
+ baseBranch = latestRelease;
103
+ logger.success(`自动选择目标分支: ${baseBranch}\n`);
104
+ } else {
105
+ logger.error('未侦测到任何 release 分支');
106
+ console.log('💡 使用 --base 参数指定目标分支');
107
+ process.exit(1);
108
+ }
196
109
  }
197
110
 
198
- if (!GitHubAPI.isAvailable()) {
199
- logger.error('GitHub CLI 未安裝或未認證');
200
- logger.info('請先安裝並設定 GitHub CLI:');
201
- logger.info(' brew install gh');
202
- logger.info(' gh auth login');
111
+ // 检查是否在 base branch
112
+ if (currentBranch === baseBranch) {
113
+ logger.error(`你目前在 ${baseBranch} 分支,请切换到 feature 分支`);
114
+ console.log('💡 切换到 feature 分支: git checkout <feature-branch>');
203
115
  process.exit(1);
204
116
  }
205
117
 
206
- // 載入配置
207
- const config = await loadConfig(options);
208
-
209
- // 確定分支
210
- const headBranch = config.headBranch || GitOperations.getCurrentBranch();
211
- const baseBranch = await determineBaseBranch(config, logger);
212
-
213
- console.log(chalk.cyan(`\n📌 分支資訊:`));
118
+ console.log('\n📌 分支资讯:');
214
119
  console.log(` Base: ${baseBranch}`);
215
- console.log(` Head: ${headBranch}`);
216
-
217
- // 檢查 PR 是否已存在
218
- if (GitHubAPI.prExists(baseBranch, headBranch)) {
219
- logger.warn('PR 已存在');
220
- process.exit(0);
120
+ console.log(` Head: ${headBranch}\n`);
121
+
122
+ // 推送到远端(预览模式跳过)
123
+ if (!config.preview) {
124
+ logger.step(`推送到远端分支: origin/${headBranch}`);
125
+ GitOperations.push(headBranch);
126
+ logger.success('推送成功\n');
127
+
128
+ // 等待 GitHub 同步
129
+ logger.info('等待 GitHub 同步...');
130
+ await new Promise(resolve => setTimeout(resolve, 2000));
131
+ GitOperations.fetch();
132
+ logger.success('同步完成\n');
221
133
  }
222
134
 
223
- // 生成 PR 內容
224
- const prContent = await generatePRContent(baseBranch, headBranch, config, logger);
135
+ // 获取变更统计
136
+ const stats = GitOperations.getChangeStats(baseBranch, headBranch);
137
+ console.log(`📈 变更统计: ${stats.stats}`);
138
+ console.log(`📁 影响文件: ${stats.filesChanged} 个\n`);
225
139
 
226
- console.log(chalk.cyan('\n📝 PR 標題:'));
227
- logger.code(prContent.title);
140
+ // 生成 PR 内容
141
+ const prContent = await generatePRContent(baseBranch, headBranch, config);
228
142
 
229
- console.log(chalk.cyan('\n📄 PR 描述:'));
230
- logger.code(prContent.description);
143
+ // 显示预览
144
+ displayPreview(prContent, stats);
231
145
 
232
- // 如果是預覽模式,不創建 PR
146
+ // 预览模式:仅显示不创建
233
147
  if (config.preview) {
234
- logger.info('預覽模式,不創建 PR');
235
- process.exit(0);
148
+ logger.info('预览模式:未创建 PR');
149
+ return;
236
150
  }
237
151
 
238
- // 確認是否創建 PR
239
- if (!config.noConfirm) {
240
- const { confirm } = await inquirer.prompt([
241
- {
242
- type: 'confirm',
243
- name: 'confirm',
244
- message: '是否創建 PR?',
245
- default: true,
246
- },
247
- ]);
248
-
249
- if (!confirm) {
250
- logger.info('已取消');
251
- process.exit(0);
252
- }
253
- }
152
+ // 创建 PR(使用 GitHub CLI)
153
+ logger.step('创建 Pull Request...');
254
154
 
255
- // 選擇 Reviewers
256
- let reviewers = [];
257
- if (config.reviewers.autoSelect) {
258
- const changes = GitOperations.getAllChanges();
259
- const changedFiles = changes.map(c => c.filePath);
260
- reviewers = await selectReviewers(changedFiles, config, logger);
155
+ // 准备 PR body
156
+ const prBody = prContent.description;
157
+
158
+ // 构建 gh pr create 命令
159
+ let ghCommand = `gh pr create --base ${baseBranch} --head ${headBranch} --title "${prContent.title.replace(/"/g, '\\"')}"`;
160
+
161
+ // 将 body 写入临时文件
162
+ const { writeFileSync, unlinkSync } = await import('fs');
163
+ const tmpFile = '/tmp/pr-body-temp.md';
164
+ writeFileSync(tmpFile, prBody, 'utf-8');
165
+ ghCommand += ` --body-file ${tmpFile}`;
166
+
167
+ if (config.draft) {
168
+ ghCommand += ' --draft';
261
169
  }
262
170
 
263
- // 選擇 Labels
264
- let labels = [];
265
- if (config.github.autoLabels) {
266
- const availableLabels = GitHubAPI.getLabels();
267
- const suggestedLabels = suggestLabels(prContent, availableLabels);
268
-
269
- if (suggestedLabels.length > 0) {
270
- const { selectedLabels } = await inquirer.prompt([
271
- {
272
- type: 'checkbox',
273
- name: 'selectedLabels',
274
- message: '選擇 Labels:',
275
- choices: suggestedLabels.map(label => ({
276
- name: label,
277
- value: label,
278
- checked: true,
279
- })),
280
- },
281
- ]);
282
- labels = selectedLabels;
171
+ try {
172
+ const result = execSync(ghCommand, { encoding: 'utf-8' });
173
+ unlinkSync(tmpFile);
174
+
175
+ logger.success('PR 创建成功!\n');
176
+ console.log(result);
177
+ } catch (error) {
178
+ try {
179
+ unlinkSync(tmpFile);
180
+ } catch (e) {
181
+ // 忽略
283
182
  }
183
+ throw new Error(`创建 PR 失败: ${error.message}`);
284
184
  }
285
-
286
- // 創建 PR
287
- logger.startSpinner('正在創建 PR...');
288
-
289
- GitHubAPI.createPR({
290
- title: prContent.title,
291
- body: prContent.description,
292
- base: baseBranch,
293
- head: headBranch,
294
- draft: config.draft,
295
- reviewers,
296
- labels,
297
- });
298
-
299
- logger.succeedSpinner('PR 創建成功!');
300
-
301
185
  } catch (error) {
302
186
  handleError(error);
303
187
  process.exit(1);
@@ -1,115 +1,50 @@
1
1
  /**
2
- * AI Client
3
- *
4
- * 封裝 GitHub Copilot SDK 的 AI 客戶端
2
+ * AI 客户端
3
+ * 基于 @github/copilot-sdk
5
4
  */
6
5
 
7
6
  import { CopilotClient } from '@github/copilot-sdk';
8
7
 
9
8
  export class AIClient {
10
- constructor(config = {}) {
11
- this.config = config;
12
- this.client = null;
13
- this.session = null;
14
- }
15
-
16
- /**
17
- * 初始化客戶端
18
- */
19
- async initialize() {
20
- if (!this.client) {
21
- this.client = new CopilotClient();
22
- }
23
- return this.client;
24
- }
25
-
26
9
  /**
27
- * 創建會話
10
+ * 发送 prompt 并等待回应(带重试机制)
28
11
  */
29
- async createSession(options = {}) {
30
- await this.initialize();
31
-
32
- this.session = await this.client.createSession({
33
- model: options.model || this.config.ai?.model || 'gpt-4.1',
34
- ...options,
35
- });
36
-
37
- return this.session;
38
- }
39
-
40
- /**
41
- * 發送請求並等待回應(帶重試機制)
42
- */
43
- async sendAndWait(prompt, options = {}) {
44
- const maxRetries = options.maxRetries || this.config.ai?.maxRetries || 3;
12
+ static async sendAndWait(prompt, model = 'gpt-4.1', maxRetries = 3) {
13
+ const client = new CopilotClient();
45
14
  let lastError = null;
46
15
 
47
16
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
48
17
  try {
49
- if (!this.session) {
50
- await this.createSession(options);
51
- }
18
+ const session = await client.createSession({ model });
19
+ const response = await session.sendAndWait({ prompt });
20
+ await client.stop();
52
21
 
53
- const response = await this.session.sendAndWait({ prompt });
54
- return response?.data?.content || '';
22
+ const content = response?.data?.content || '';
23
+ return content.trim();
55
24
  } catch (error) {
56
25
  lastError = error;
57
-
58
- if (this.config.output?.verbose) {
59
- console.log(`⚠️ 嘗試 ${attempt}/${maxRetries} 失敗: ${error.message}`);
60
- }
61
-
62
- // 如果還有重試機會,重新創建 session
63
26
  if (attempt < maxRetries) {
64
- this.session = null;
27
+ console.log(`⚠️ AI 请求失败,重试第 ${attempt}/${maxRetries} 次...`);
28
+ await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
65
29
  continue;
66
30
  }
67
31
  }
68
32
  }
69
33
 
70
- throw new Error(`AI 請求失敗(嘗試 ${maxRetries} 次): ${lastError?.message || '未知錯誤'}`);
34
+ throw new Error(`AI 请求失败: ${lastError?.message || '未知错误'}`);
71
35
  }
72
36
 
73
37
  /**
74
- * 停止客戶端
38
+ * 解析 JSON 响应
75
39
  */
76
- async stop() {
77
- if (this.client) {
78
- await this.client.stop();
79
- this.client = null;
80
- this.session = null;
81
- }
82
- }
83
-
84
- /**
85
- * 清理回應內容(移除 markdown 程式碼區塊等)
86
- */
87
- static cleanResponse(response) {
88
- if (!response) return '';
89
-
90
- let cleaned = response.trim();
40
+ static parseJSON(content) {
41
+ // 移除可能的 markdown code block 标记
42
+ const jsonContent = content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
91
43
 
92
- // 移除 markdown 程式碼區塊標記
93
- cleaned = cleaned.replace(/^```[\w]*\n/gm, '');
94
- cleaned = cleaned.replace(/\n```$/gm, '');
95
- cleaned = cleaned.replace(/^```$/gm, '');
96
-
97
- // 移除開頭和結尾的引號
98
- cleaned = cleaned.replace(/^["']|["']$/g, '');
99
-
100
- return cleaned.trim();
101
- }
102
-
103
- /**
104
- * 解析 JSON 回應
105
- */
106
- static parseJSON(response) {
107
- const cleaned = AIClient.cleanResponse(response);
108
-
109
44
  try {
110
- return JSON.parse(cleaned);
45
+ return JSON.parse(jsonContent);
111
46
  } catch (error) {
112
- throw new Error(`無法解析 AI 回應為 JSON: ${error.message}`);
47
+ throw new Error(`无法解析 AI 回应为 JSON: ${error.message}`);
113
48
  }
114
49
  }
115
50
  }