ai-git-tools 2.0.25 → 2.0.26

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-git-tools",
3
- "version": "2.0.25",
3
+ "version": "2.0.26",
4
4
  "description": "AI-powered Git automation tools for commit messages and PR generation",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -247,56 +247,71 @@ async function commitGroup(group, files, config) {
247
247
  // 忽略錯誤(可能沒有 staged 的檔案)
248
248
  }
249
249
 
250
- // Add 這組的檔案
251
- for (const file of files) {
252
- const fileStatus = file.isNew ? '新增' : file.isDeleted ? '刪除' : '修改';
253
- console.log(` ├─ [${fileStatus}] ${file.filePath}`);
254
- try {
255
- // 使用 JSON.stringify 來正確處理包含空格或特殊字符的檔案路徑
256
- // git add 對於刪除的檔案也能正確處理
257
- execSync(`git add ${JSON.stringify(file.filePath)}`, { encoding: 'utf-8' });
258
- } catch (addError) {
259
- console.error(` ⚠️ 無法加入檔案: ${file.filePath}`, addError.message);
260
- throw addError;
250
+ // Add 這組的檔案(使用 try-finally 確保失敗時清理)
251
+ const addedFiles = [];
252
+ try {
253
+ for (const file of files) {
254
+ const fileStatus = file.isNew ? '新增' : file.isDeleted ? '刪除' : '修改';
255
+ console.log(` ├─ [${fileStatus}] ${file.filePath}`);
256
+ try {
257
+ // 使用 JSON.stringify 來正確處理包含空格或特殊字符的檔案路徑
258
+ // git add 對於刪除的檔案也能正確處理
259
+ execSync(`git add ${JSON.stringify(file.filePath)}`, { encoding: 'utf-8' });
260
+ addedFiles.push(file.filePath);
261
+ } catch (addError) {
262
+ console.error(` ⚠️ 無法加入檔案: ${file.filePath}`, addError.message);
263
+ throw addError;
264
+ }
261
265
  }
262
- }
263
266
 
264
- // 生成 commit message
265
- console.log(` └─ 生成 commit message...`);
266
- const commitMessage = await generateCommitMessage(group, files, config);
267
+ // 生成 commit message
268
+ console.log(` └─ 生成 commit message...`);
269
+ const commitMessage = await generateCommitMessage(group, files, config);
267
270
 
268
- if (!commitMessage) {
269
- console.log(` ❌ 無法生成 commit message,跳過此群組`);
270
- return false;
271
- }
272
-
273
- console.log(`\n 📝 Commit Message:`);
274
- console.log(` ${'─'.repeat(50)}`);
275
- commitMessage.split('\n').forEach((line) => {
276
- console.log(` ${line}`);
277
- });
278
- console.log(` ${'─'.repeat(50)}`);
271
+ if (!commitMessage) {
272
+ console.log(` ❌ 無法生成 commit message,跳過此群組`);
273
+ return false;
274
+ }
279
275
 
280
- // 執行 commit
281
- // 使用暫存檔案避免 commit message 中的特殊字元問題
282
- const tmpFile = '.git/COMMIT_EDITMSG_TMP';
283
- try {
284
- writeFileSync(tmpFile, commitMessage, 'utf-8');
285
- execSync(`git commit -F ${tmpFile}`, {
286
- stdio: 'inherit',
276
+ console.log(`\n 📝 Commit Message:`);
277
+ console.log(` ${'─'.repeat(50)}`);
278
+ commitMessage.split('\n').forEach((line) => {
279
+ console.log(` ${line}`);
287
280
  });
288
- unlinkSync(tmpFile);
289
- } catch (commitError) {
281
+ console.log(` ${'─'.repeat(50)}`);
282
+
283
+ // 執行 commit
284
+ // 使用暫存檔案避免 commit message 中的特殊字元問題
285
+ const tmpFile = '.git/COMMIT_EDITMSG_TMP';
290
286
  try {
287
+ writeFileSync(tmpFile, commitMessage, 'utf-8');
288
+ execSync(`git commit -F ${tmpFile}`, {
289
+ stdio: 'inherit',
290
+ });
291
291
  unlinkSync(tmpFile);
292
- } catch (e) {
293
- // 忽略刪除暫存檔案的錯誤
292
+ } catch (commitError) {
293
+ try {
294
+ unlinkSync(tmpFile);
295
+ } catch (e) {
296
+ // 忽略刪除暫存檔案的錯誤
297
+ }
298
+ throw commitError;
294
299
  }
295
- throw commitError;
296
- }
297
300
 
298
- console.log(` ✅ Commit 完成!`);
299
- return true;
301
+ console.log(` ✅ Commit 完成!`);
302
+ return true;
303
+ } catch (error) {
304
+ // 如果失敗,unstage 所有已經 add 的檔案
305
+ if (addedFiles.length > 0) {
306
+ console.log(` 🔄 清理已 staged 的檔案...`);
307
+ try {
308
+ execSync('git reset HEAD -- .', { stdio: 'ignore' });
309
+ } catch (e) {
310
+ // 忽略 reset 錯誤
311
+ }
312
+ }
313
+ throw error;
314
+ }
300
315
  } catch (error) {
301
316
  console.error(` ❌ Commit 失敗:`, error.message);
302
317
  return false;
@@ -329,7 +344,7 @@ export async function commitAllCommand() {
329
344
 
330
345
  if (changes.length === 0) {
331
346
  logger.info('沒有需要提交的變更');
332
- process.exit(0);
347
+ return;
333
348
  }
334
349
 
335
350
  console.log(`📊 找到 ${changes.length} 個變更的檔案:\n`);
@@ -346,7 +361,7 @@ export async function commitAllCommand() {
346
361
 
347
362
  if (!groups || groups.length === 0) {
348
363
  logger.error('AI 分析失敗或沒有產生分組');
349
- process.exit(1);
364
+ throw new Error('AI 分析失敗或沒有產生分組');
350
365
  }
351
366
 
352
367
  // 驗證所有檔案都被包含在分組中
@@ -441,6 +456,6 @@ export async function commitAllCommand() {
441
456
  }
442
457
  } catch (error) {
443
458
  handleError(error);
444
- process.exit(1);
459
+ throw error;
445
460
  }
446
461
  }
@@ -31,7 +31,7 @@ export async function commitCommand() {
31
31
  if (!diff.trim()) {
32
32
  logger.error('沒有 staged 的變更');
33
33
  console.log('💡 請先使用 git add 來 stage 你的變更');
34
- process.exit(1);
34
+ throw new Error('沒有 staged 的變更');
35
35
  }
36
36
 
37
37
  logger.step('正在分析變更內容...\n');
@@ -115,7 +115,7 @@ ${truncatedDiff}`;
115
115
  console.log(' 1. 檢查網路連線');
116
116
  console.log(' 2. 嘗試更換 AI 模型(使用 --model 參數)');
117
117
  console.log(' 3. 確認變更內容不會太複雜或太大');
118
- process.exit(1);
118
+ throw new Error('無法產生有效的 commit message');
119
119
  }
120
120
 
121
121
  logger.success('產生的 Commit Message:');
@@ -132,6 +132,6 @@ ${truncatedDiff}`;
132
132
  logger.success('Commit 完成!\n');
133
133
  } catch (error) {
134
134
  handleError(error);
135
- process.exit(1);
135
+ throw error;
136
136
  }
137
137
  }
@@ -33,6 +33,6 @@ export async function prCommand() {
33
33
  await workflow.execute();
34
34
  } catch (error) {
35
35
  handleError(error);
36
- process.exit(1);
36
+ throw error;
37
37
  }
38
38
  }
@@ -7,17 +7,23 @@ import { CopilotClient } from '@github/copilot-sdk';
7
7
 
8
8
  export class AIClient {
9
9
  /**
10
- * 發送 prompt 並等待回應(帶重試機制)
10
+ * 發送 prompt 並等待回應(帶重試機制和超時保護)
11
11
  */
12
- static async sendAndWait(prompt, model = 'gpt-4.1', maxRetries = 3) {
13
- const client = new CopilotClient();
12
+ static async sendAndWait(prompt, model = 'gpt-4.1', maxRetries = 3, timeout = 60000) {
14
13
  let lastError = null;
15
14
 
16
15
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
16
+ const client = new CopilotClient();
17
17
  try {
18
18
  const session = await client.createSession({ model });
19
- const response = await session.sendAndWait({ prompt });
20
- await client.stop();
19
+
20
+ // 使用 Promise.race 實現超時控制
21
+ const responsePromise = session.sendAndWait({ prompt });
22
+ const timeoutPromise = new Promise((_, reject) => {
23
+ setTimeout(() => reject(new Error(`AI 請求超時 (${timeout}ms)`)), timeout);
24
+ });
25
+
26
+ const response = await Promise.race([responsePromise, timeoutPromise]);
21
27
 
22
28
  const content = response?.data?.content || '';
23
29
  return content.trim();
@@ -26,7 +32,13 @@ export class AIClient {
26
32
  if (attempt < maxRetries) {
27
33
  console.log(`⚠️ AI 請求失敗,重試第 ${attempt}/${maxRetries} 次...`);
28
34
  await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
29
- continue;
35
+ }
36
+ } finally {
37
+ // 確保每次都關閉 client,無論成功或失敗
38
+ try {
39
+ await client.stop();
40
+ } catch (e) {
41
+ // 忽略關閉錯誤
30
42
  }
31
43
  }
32
44
  }
@@ -29,19 +29,27 @@ export class AIAnalyzer {
29
29
  const { client, session } = await this.createClient();
30
30
 
31
31
  try {
32
- const response = await session.sendAndWait({ prompt });
33
- const prContent = response?.data.content?.trim() || '';
32
+ // 使用超時保護 (60 )
33
+ const responsePromise = session.sendAndWait({ prompt });
34
+ const timeoutPromise = new Promise((_, reject) => {
35
+ setTimeout(() => reject(new Error('AI 請求超時 (60 秒)')), 60000);
36
+ });
34
37
 
35
- await client.stop();
38
+ const response = await Promise.race([responsePromise, timeoutPromise]);
39
+ const prContent = response?.data.content?.trim() || '';
36
40
 
37
41
  if (!prContent) {
38
42
  throw new Error('AI 未能生成 PR 內容');
39
43
  }
40
44
 
41
45
  return this.parsePRContent(prContent);
42
- } catch (error) {
43
- await client.stop();
44
- throw error;
46
+ } finally {
47
+ // 確保 client 一定會被關閉
48
+ try {
49
+ await client.stop();
50
+ } catch (e) {
51
+ // 忽略關閉錯誤
52
+ }
45
53
  }
46
54
  }
47
55
 
@@ -56,10 +64,15 @@ export class AIAnalyzer {
56
64
 
57
65
  try {
58
66
  log.info(' 正在使用 AI 深度分析程式碼變更...');
59
- const response = await session.sendAndWait({ prompt });
60
- const content = response?.data.content?.trim() || '';
67
+
68
+ // 使用超時保護 (60 )
69
+ const responsePromise = session.sendAndWait({ prompt });
70
+ const timeoutPromise = new Promise((_, reject) => {
71
+ setTimeout(() => reject(new Error('AI 請求超時 (60 秒)')), 60000);
72
+ });
61
73
 
62
- await client.stop();
74
+ const response = await Promise.race([responsePromise, timeoutPromise]);
75
+ const content = response?.data.content?.trim() || '';
63
76
 
64
77
  // 解析 JSON
65
78
  try {
@@ -85,9 +98,15 @@ export class AIAnalyzer {
85
98
  return this.getFallbackAnalysis(changedFiles);
86
99
  }
87
100
  } catch (error) {
88
- await client.stop();
89
101
  log.warning(` AI 分析失敗 (${error.message}),使用基礎分析...\n`);
90
102
  return this.getFallbackAnalysis(changedFiles);
103
+ } finally {
104
+ // 確保 client 一定會被關閉
105
+ try {
106
+ await client.stop();
107
+ } catch (e) {
108
+ // 忽略關閉錯誤
109
+ }
91
110
  }
92
111
  }
93
112
 
@@ -49,14 +49,16 @@ export class GitOperations {
49
49
  /**
50
50
  * 獲取變更統計
51
51
  */
52
- getChangeStats(baseBranch, headBranch) {
52
+ getChangeStats(baseBranch, headBranch, useRemoteHead = false) {
53
53
  try {
54
- const stats = execSync(`git diff --shortstat origin/${baseBranch}...${headBranch}`, {
54
+ // 使用本地 headBranch 以確保能檢測到未推送的變更
55
+ const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
56
+ const stats = execSync(`git diff --shortstat origin/${baseBranch}...${headRef}`, {
55
57
  encoding: 'utf-8',
56
58
  }).trim();
57
59
 
58
60
  const filesChanged = execSync(
59
- `git diff --name-only origin/${baseBranch}...${headBranch} | wc -l`,
61
+ `git diff --name-only origin/${baseBranch}...${headRef} | wc -l`,
60
62
  { encoding: 'utf-8' }
61
63
  ).trim();
62
64
 
@@ -69,9 +71,11 @@ export class GitOperations {
69
71
  /**
70
72
  * 獲取變更的檔案列表
71
73
  */
72
- getChangedFiles(baseBranch, headBranch) {
74
+ getChangedFiles(baseBranch, headBranch, useRemoteHead = false) {
73
75
  try {
74
- const files = execSync(`git diff --name-only origin/${baseBranch}...${headBranch}`, {
76
+ // 使用本地 headBranch 以確保能檢測到未推送的變更
77
+ const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
78
+ const files = execSync(`git diff --name-only origin/${baseBranch}...${headRef}`, {
75
79
  encoding: 'utf-8',
76
80
  })
77
81
  .split('\n')
@@ -86,9 +90,11 @@ export class GitOperations {
86
90
  * 獲取 commit 列表
87
91
  */
88
92
  getCommits(baseBranch, headBranch, options = {}) {
89
- const { oneline = true, noDecorate = true } = options;
93
+ const { oneline = true, noDecorate = true, useRemoteHead = false } = options;
90
94
  try {
91
- let cmd = `git log origin/${baseBranch}..origin/${headBranch}`;
95
+ // 使用本地 headBranch 以確保能檢測到未推送的 commit
96
+ const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
97
+ let cmd = `git log origin/${baseBranch}..${headRef}`;
92
98
  if (oneline) cmd += ' --oneline';
93
99
  if (noDecorate) cmd += ' --no-decorate';
94
100
 
@@ -98,7 +104,7 @@ export class GitOperations {
98
104
  '無法比較分支差異',
99
105
  'GIT_COMPARE_FAILED',
100
106
  ['檢查遠端分支是否存在: git branch -r', '執行診斷: npm run diagnose:pr'],
101
- `git log origin/${baseBranch}..origin/${headBranch}`
107
+ `git log origin/${baseBranch}..${headBranch}`
102
108
  );
103
109
  }
104
110
  }
@@ -106,15 +112,18 @@ export class GitOperations {
106
112
  /**
107
113
  * 獲取 diff
108
114
  */
109
- getDiff(baseBranch, headBranch, maxBuffer = CONSTANTS.MAX_BUFFER_SIZE) {
115
+ getDiff(baseBranch, headBranch, useRemoteHead = false, maxBuffer = CONSTANTS.MAX_BUFFER_SIZE) {
110
116
  try {
111
- return execSync(`git diff origin/${baseBranch}...${headBranch}`, {
117
+ // 使用本地 headBranch 以確保能檢測到未推送的變更
118
+ const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
119
+ return execSync(`git diff origin/${baseBranch}...${headRef}`, {
112
120
  encoding: 'utf-8',
113
121
  maxBuffer,
114
122
  });
115
123
  } catch (error) {
116
124
  // 嘗試替代方案
117
- return execSync(`git diff origin/${baseBranch}..${headBranch}`, {
125
+ const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
126
+ return execSync(`git diff origin/${baseBranch}..${headRef}`, {
118
127
  encoding: 'utf-8',
119
128
  maxBuffer,
120
129
  });
@@ -1,4 +1,5 @@
1
1
  import { createInterface } from 'readline';
2
+ import { execSync } from 'child_process';
2
3
  import { GitOperations } from './git-operations.js';
3
4
  import { GitHubAPI } from './github-api.js';
4
5
  import { AIAnalyzer } from '../ai/code-analyzer.js';
@@ -202,12 +203,35 @@ export class PRWorkflow {
202
203
  await this.git.fetch();
203
204
  log.success('同步完成\n');
204
205
 
205
- // 檢查本地 commit 差異
206
+ // 顯示分支狀態診斷信息
207
+ if (this.config.output?.verbose) {
208
+ console.log(`${colors.cyan}🔍 分支診斷信息:${colors.reset}`);
209
+ try {
210
+ const currentBranch = this.git.getCurrentBranch();
211
+ console.log(` 當前分支: ${currentBranch}`);
212
+ console.log(` Base 分支: origin/${baseBranch}`);
213
+ console.log(` Head 分支: ${headBranch} (本地)`);
214
+
215
+ // 檢查遠端分支是否存在
216
+ try {
217
+ execSync(`git rev-parse --verify origin/${headBranch}`, { stdio: 'pipe' });
218
+ console.log(` 遠端 ${headBranch}: ✓ 存在`);
219
+ } catch (e) {
220
+ console.log(` 遠端 ${headBranch}: ✗ 不存在(尚未推送)`);
221
+ }
222
+ } catch (e) {
223
+ // 忽略診斷錯誤
224
+ }
225
+ console.log('');
226
+ }
227
+
228
+ // 檢查本地 commit 差異(使用本地 headBranch)
206
229
  let localCommits;
207
230
  try {
208
231
  localCommits = this.git.getCommits(baseBranch, headBranch, {
209
232
  oneline: true,
210
233
  noDecorate: true,
234
+ useRemoteHead: false, // 使用本地分支以檢測未推送的commit
211
235
  });
212
236
  } catch (error) {
213
237
  throw new PRError(
@@ -261,10 +285,11 @@ export class PRWorkflow {
261
285
  * 收集變更資料
262
286
  */
263
287
  collectChangeData(baseBranch, headBranch) {
264
- const stats = this.git.getChangeStats(baseBranch, headBranch);
265
- const changedFiles = this.git.getChangedFiles(baseBranch, headBranch);
266
- const commits = this.git.getCommits(baseBranch, headBranch);
267
- const diff = this.git.getDiff(baseBranch, headBranch);
288
+ // 此時已經推送完成,使用本地分支即可(本地和遠端應該已同步)
289
+ const stats = this.git.getChangeStats(baseBranch, headBranch, false);
290
+ const changedFiles = this.git.getChangedFiles(baseBranch, headBranch, false);
291
+ const commits = this.git.getCommits(baseBranch, headBranch, { useRemoteHead: false });
292
+ const diff = this.git.getDiff(baseBranch, headBranch, false);
268
293
  const truncatedDiff = this.git.truncateDiff(diff);
269
294
 
270
295
  console.log(`📈 變更統計: ${stats.stats}`);
@@ -59,6 +59,7 @@ export class InteractiveSelect {
59
59
  const cleanup = () => {
60
60
  process.stdin.setRawMode(false);
61
61
  process.stdin.removeAllListeners('keypress');
62
+ process.stdin.pause(); // 確保 stdin stream 被關閉
62
63
  process.stdout.write(cursor.show);
63
64
  console.log('');
64
65
  };