ai-git-tools 2.0.45 → 2.0.47

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/bin/cli.js CHANGED
@@ -108,6 +108,7 @@ program
108
108
  .option('--skip-pr', '跳過自動 PR 建立')
109
109
  .option('--skip-all', '只執行測試並發佈評論')
110
110
  .option('--commit-only', '執行測試 + commit,不建立 PR')
111
+ .option('--skip-tests', '無測試可執行時仍繼續 commit/PR(不強制停止)')
111
112
  .action(async (issue, options) => {
112
113
  try {
113
114
  await autodevCommand(issue, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-git-tools",
3
- "version": "2.0.45",
3
+ "version": "2.0.47",
4
4
  "description": "AI-powered Git automation tools for commit messages and PR generation",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -40,6 +40,7 @@ export async function autodevCommand(issueInput, cliOptions = {}) {
40
40
  skipPr: cliOptions.skipPr ?? config.skipPr ?? false,
41
41
  skipAll: cliOptions.skipAll ?? false,
42
42
  commitOnly: cliOptions.commitOnly ?? false,
43
+ skipTests: cliOptions.skipTests ?? config.skipTests ?? false,
43
44
  };
44
45
 
45
46
  // 永遠顯示啟動設定,方便確認
@@ -49,6 +50,7 @@ export async function autodevCommand(issueInput, cliOptions = {}) {
49
50
  console.log(` 測試框架 : ${options.framework ?? '自動偵測'}`);
50
51
  console.log(` Commit : ${options.skipAll || options.skipCommit ? '跳過' : '自動'}`);
51
52
  console.log(` PR : ${options.skipAll || options.skipPr || options.commitOnly ? '跳過' : '自動'}`);
53
+ console.log(` 跳過測試 : ${options.skipTests ? '是(--skip-tests)' : '否(0 個測試時停止)'}`);
52
54
  if (options.dryRun) console.log(' ⚠️ 乾運行模式(不實際執行)');
53
55
 
54
56
  const workflow = new AutodevWorkflow(config);
@@ -7,39 +7,52 @@ import { CopilotClient } from '@github/copilot-sdk';
7
7
 
8
8
  export class AIClient {
9
9
  /**
10
- * 發送 prompt 並等待回應(帶重試機制和超時保護)
10
+ * 發送 prompt 並等待回應(帶重試機制)
11
+ *
12
+ * @param {string} prompt
13
+ * @param {string} model
14
+ * @param {number} maxRetries
15
+ * @param {number} timeout - 毫秒,會直接傳給 SDK 的 sendAndWait
11
16
  */
12
- static async sendAndWait(prompt, model = 'gpt-4.1', maxRetries = 3, timeout = 60000) {
17
+ static async sendAndWait(prompt, model = 'claude-sonnet-4.5', maxRetries = 3, timeout = 60000) {
13
18
  let lastError = null;
14
19
 
15
20
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
16
21
  const client = new CopilotClient();
22
+ let session = null;
17
23
  try {
18
- const session = await client.createSession({ model });
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
+ session = await client.createSession({ model });
25
+
26
+ // 收集 session.error 事件(讓 lastError 包含細節)
27
+ session.on('session.error', (event) => {
28
+ lastError = new Error(event.data?.message || JSON.stringify(event.data));
24
29
  });
25
30
 
26
- const response = await Promise.race([responsePromise, timeoutPromise]);
31
+ // timeout 直接傳給 SDK,讓 SDK 管理逾時
32
+ const response = await session.sendAndWait({ prompt }, timeout);
33
+
34
+ // response 為 undefined 表示 session 結束但沒有助理回訊(通常是錯誤)
35
+ if (!response) {
36
+ throw new Error(`AI 未回傳任何訊息(model: ${model})`);
37
+ }
27
38
 
28
39
  const content = response?.data?.content || '';
40
+ if (!content) {
41
+ throw new Error('AI 回傳空內容');
42
+ }
29
43
  return content.trim();
44
+
30
45
  } catch (error) {
31
46
  lastError = error;
32
47
  if (attempt < maxRetries) {
33
- console.log(`⚠️ AI 請求失敗,重試第 ${attempt}/${maxRetries} 次...`);
48
+ console.log(`⚠️ AI 請求失敗,重試第 ${attempt}/${maxRetries} 次... (${error.message})`);
34
49
  await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
35
50
  }
36
51
  } finally {
37
- // 確保每次都關閉 client,無論成功或失敗
38
- try {
39
- await client.stop();
40
- } catch (e) {
41
- // 忽略關閉錯誤
52
+ if (session) {
53
+ try { await session.destroy(); } catch (_e) { /* 忽略 */ }
42
54
  }
55
+ try { await client.stop(); } catch (_e) { /* 忽略 */ }
43
56
  }
44
57
  }
45
58
 
@@ -28,25 +28,46 @@ export class CodeGenerator {
28
28
  async generateCodeFiles(issueData, projectRoot = process.cwd()) {
29
29
  const prompt = this._buildPrompt(issueData, projectRoot);
30
30
 
31
- const VALID_MODELS = ['gpt-4.1', 'gpt-4o', 'claude-haiku-4.5', 'claude-sonnet-4.5', 'o3', 'o4-mini'];
31
+ const VALID_MODELS = [
32
+ 'gpt-4.1',
33
+ 'gpt-4o',
34
+ 'claude-haiku-4.5',
35
+ 'claude-sonnet-4.5',
36
+ 'claude-sonnet-4.6',
37
+ 'o3',
38
+ 'o4-mini',
39
+ ];
32
40
  if (!VALID_MODELS.includes(this.model)) {
33
41
  console.warn(` ⚠️ model「${this.model}」可能無效!支援的模型:${VALID_MODELS.join(', ')}`);
34
42
  }
35
- console.log(` 🤖 調用 AI 生成代碼框架(model: ${this.model},最多等待 3 分鐘)...`);
43
+
44
+ // Claude 模型使用 extended thinking,需要更長的等待時間
45
+ const isClaude = this.model.toLowerCase().includes('claude');
46
+ const timeout = isClaude ? 300_000 : 120_000; // Claude 5 分鐘,GPT 2 分鐘
47
+ const timeoutMinutes = timeout / 60_000;
48
+
49
+ console.log(` 🤖 調用 AI 生成代碼框架(model: ${this.model},最多等待 ${timeoutMinutes} 分鐘)...`);
50
+ if (isClaude) {
51
+ console.log(' ℹ️ Claude 模型使用 extended thinking 模式,回應時間較長,請耐心等候');
52
+ console.log(' 💡 若等待太久,可改用較快的模型:在 .ai-git-config 的 autodev.aiModel 改為 "gpt-4.1"');
53
+ }
36
54
 
37
55
  let response;
38
56
  try {
39
- // 代碼生成需要較長時間,使用 180 秒 timeout
40
57
  response = await AIClient.sendAndWait(
41
58
  prompt,
42
59
  this.model,
43
60
  this.config.maxRetries || 3,
44
- 180_000
61
+ timeout
45
62
  );
46
63
  } catch (error) {
47
- // 顯示完整錯誤訊息方便除錯
48
- console.warn(` ⚠️ AI 調用失敗:${error.message}`);
49
- if (error.cause) console.warn(` 原因:${error.cause}`);
64
+ console.warn(` ❌ AI 調用最終失敗:${error.message}`);
65
+ console.warn(' 💡 排查建議:');
66
+ console.warn(` 1. 確認 model「${this.model}」已在你的 Copilot 方案中啟用`);
67
+ console.warn(
68
+ ' 2. 嘗試改用 gpt-4.1:在 .ai-git-config 的 autodev.aiModel 改為 "gpt-4.1"'
69
+ );
70
+ console.warn(' 3. 確認 gh auth status 有登入 GitHub');
50
71
  return [];
51
72
  }
52
73
 
@@ -66,8 +87,9 @@ export class CodeGenerator {
66
87
  }
67
88
  } else {
68
89
  console.log(` ✅ AI 規劃生成 ${files.length} 個檔案:`);
69
- files.forEach((f) => {
70
- const icon = f.filePath.includes('.test.') || f.filePath.includes('__tests__') ? '🧪' : '📄';
90
+ files.forEach(f => {
91
+ const icon =
92
+ f.filePath.includes('.test.') || f.filePath.includes('__tests__') ? '🧪' : '📄';
71
93
  console.log(` ${icon} ${f.filePath}`);
72
94
  });
73
95
  }
@@ -206,7 +228,7 @@ ${body}
206
228
  return [];
207
229
  }
208
230
 
209
- return files.map((f) => ({
231
+ return files.map(f => ({
210
232
  filePath: f.filePath || '',
211
233
  content: f.content || '',
212
234
  type: f.type || 'new',
@@ -21,6 +21,7 @@ import { prProgrammatic } from '../../commands/pr.js';
21
21
  * @property {boolean} [skipPr] - 跳過 PR 建立
22
22
  * @property {boolean} [skipAll] - 只測試 + 發佈評論,跳過 commit 和 PR
23
23
  * @property {boolean} [commitOnly] - 測試 + commit,不建立 PR
24
+ * @property {boolean} [skipTests] - 無測試可執行時繼續(而非停止)
24
25
  */
25
26
 
26
27
  export class AutodevWorkflow {
@@ -127,9 +128,20 @@ export class AutodevWorkflow {
127
128
  return;
128
129
  }
129
130
 
130
- // 若沒有測試,給警告但繼續
131
+ // 若沒有測試:預設停止,加 --skip-tests 才繼續
131
132
  if (testResults.total === 0) {
132
- console.warn(' ⚠️ 無測試可執行,繼續後續流程');
133
+ if (options.skipTests) {
134
+ console.warn(' ⚠️ 無測試可執行(--skip-tests 已啟用,繼續後續流程)');
135
+ } else {
136
+ console.log('\n⏸️ 無測試可執行,已停止自動 commit / PR');
137
+ console.log(' 可能原因:');
138
+ console.log(' 1. 生成的測試檔案依賴未安裝(node_modules 缺失或模組不存在)');
139
+ console.log(' 2. 生成的測試檔案語法有誤(用 npx jest --listTests 確認)');
140
+ console.log(' 3. testPaths 設定範圍未涵蓋新生成的檔案');
141
+ console.log('\n 若確認要跳過測試直接 commit/PR,可加上 --skip-tests:');
142
+ console.log(' ai autodev <issue> --skip-tests');
143
+ return;
144
+ }
133
145
  }
134
146
 
135
147
  // ── 9. 自動 commit-all ───────────────────────────────────
@@ -4,6 +4,8 @@
4
4
  */
5
5
 
6
6
  import { execSync } from 'child_process';
7
+ import { existsSync } from 'fs';
8
+ import { join } from 'path';
7
9
  import { ExecutorBase } from './executor-base.js';
8
10
 
9
11
  export class JestExecutor extends ExecutorBase {
@@ -22,6 +24,25 @@ export class JestExecutor extends ExecutorBase {
22
24
  async run(testPaths = [], options = {}) {
23
25
  const pathArgs = testPaths.length > 0 ? testPaths.join(' ') : '';
24
26
  const cmd = `npx jest --json --forceExit --passWithNoTests ${pathArgs}`;
27
+ const projectRoot = options.projectRoot || process.cwd();
28
+
29
+ // 若 node_modules 不存在,自動執行 npm install 安裝依賴
30
+ const nodeModulesPath = join(projectRoot, 'node_modules');
31
+ if (!existsSync(nodeModulesPath)) {
32
+ console.log(' 📦 偵測到 node_modules 不存在,自動執行 npm install...');
33
+ try {
34
+ execSync('npm install', {
35
+ cwd: projectRoot,
36
+ stdio: 'inherit',
37
+ timeout: 120_000, // 2 分鐘安裝時間上限
38
+ });
39
+ console.log(' ✅ 依賴安裝完成');
40
+ } catch (err) {
41
+ console.warn(` ⚠️ npm install 失敗:${err.message}`);
42
+ console.warn(' 跳過測試步驟,代碼已生成但未驗證');
43
+ return this.emptyResults(this.framework);
44
+ }
45
+ }
25
46
 
26
47
  if (options.verbose) {
27
48
  console.log(` 執行命令:${cmd}`);
@@ -33,7 +54,7 @@ export class JestExecutor extends ExecutorBase {
33
54
  encoding: 'utf-8',
34
55
  timeout: this.timeout,
35
56
  stdio: ['pipe', 'pipe', 'pipe'],
36
- cwd: options.projectRoot || process.cwd(),
57
+ cwd: projectRoot,
37
58
  });
38
59
  } catch (error) {
39
60
  // Jest 測試失敗時 exit code 非 0,但 stdout 仍有 JSON