ai-git-tools 2.0.46 → 2.0.48

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.46",
3
+ "version": "2.0.48",
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);
@@ -40,6 +40,44 @@ export default {
40
40
  output: {
41
41
  verbose: true, // 詳細輸出
42
42
  },
43
+
44
+ // AutoDev 自動開發流程配置
45
+ // 使用方式:ai autodev <issue編號>
46
+ autodev: {
47
+ // AI 模型(可獨立於 ai.model 單獨設定,建議使用快速模型)
48
+ // 支援:'gpt-4.1'、'gpt-4o'、'claude-haiku-4.5'、'claude-sonnet-4.5'
49
+ // 注意:claude 系列使用 extended thinking,回應較慢(約 5 分鐘)
50
+ aiModel: 'gpt-4.1',
51
+
52
+ // 重試次數上限(AI 呼叫失敗時)
53
+ maxRetries: 3,
54
+
55
+ // 測試框架(null = 自動偵測,或指定 'jest' / 'vitest' / 'mocha')
56
+ framework: null,
57
+
58
+ // 測試檔案搜尋路徑(glob pattern),留空使用框架預設規則
59
+ testPaths: [
60
+ 'tests/**/*.test.js',
61
+ 'tests/**/*.spec.js',
62
+ 'src/**/__tests__/**/*.js',
63
+ ],
64
+
65
+ // 測試執行 timeout(毫秒),預設 60 秒
66
+ testTimeout: 60000,
67
+
68
+ // 目標專案根目錄(null = 當前工作目錄)
69
+ projectRoot: null,
70
+
71
+ // 無測試可執行時,是否仍繼續 commit/PR(預設 false = 停止)
72
+ // 等同 CLI 的 --skip-tests 旗標
73
+ skipTests: false,
74
+
75
+ // 跳過自動 commit(預設 false)
76
+ skipCommit: false,
77
+
78
+ // 跳過自動建立 PR(預設 false)
79
+ skipPr: false,
80
+ },
43
81
  };
44
82
  `;
45
83
 
@@ -191,9 +191,10 @@ export async function loadAutodevConfig() {
191
191
  const defaults = {
192
192
  framework: null, // null = 自動偵測
193
193
  testPaths: [], // 空 = 自動發現
194
- testTimeout: 300_000, // 5 分鐘
194
+ testTimeout: 60_000, // 1 分鐘
195
195
  skipCommit: false,
196
196
  skipPr: false,
197
+ skipTests: false,
197
198
  verbose: false,
198
199
  projectRoot: process.cwd(),
199
200
  // AI 設定(可被 autodev 子區塊或全域 ai 區塊覆蓋)
@@ -225,10 +226,9 @@ export async function loadAutodevConfig() {
225
226
 
226
227
  return {
227
228
  ...defaults,
228
- // 全域 ai 區塊優先度低於 autodev 子區塊
229
+ ...userAutodev,
230
+ // 這兩個需要跨區塊計算,放在展開之後才能保有正確優先序
229
231
  aiModel: userAutodev.aiModel ?? userAi.model ?? defaults.aiModel,
230
232
  maxRetries: userAutodev.maxRetries ?? userAi.maxRetries ?? defaults.maxRetries,
231
- // 其他 autodev 欄位
232
- ...userAutodev,
233
233
  };
234
234
  }
@@ -28,26 +28,45 @@ 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(直接傳給 SDK)
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
64
  console.warn(` ❌ AI 調用最終失敗:${error.message}`);
48
65
  console.warn(' 💡 排查建議:');
49
66
  console.warn(` 1. 確認 model「${this.model}」已在你的 Copilot 方案中啟用`);
50
- console.warn(' 2. 嘗試改用 gpt-4.1:在 .ai-git-config 的 autodev.aiModel 改為 "gpt-4.1"');
67
+ console.warn(
68
+ ' 2. 嘗試改用 gpt-4.1:在 .ai-git-config 的 autodev.aiModel 改為 "gpt-4.1"'
69
+ );
51
70
  console.warn(' 3. 確認 gh auth status 有登入 GitHub');
52
71
  return [];
53
72
  }
@@ -61,15 +80,11 @@ export class CodeGenerator {
61
80
  const files = this._parseResponse(response);
62
81
  if (files.length === 0) {
63
82
  console.warn(' ⚠️ AI 未回傳任何檔案(可能 JSON 格式有誤)');
64
- if (process.env.AUTODEV_DEBUG) {
65
- console.log(' --- AI 原始回應 ---');
66
- console.log(response.slice(0, 500));
67
- console.log(' (設定 AUTODEV_DEBUG=1 可看到完整回應)');
68
- }
69
83
  } else {
70
84
  console.log(` ✅ AI 規劃生成 ${files.length} 個檔案:`);
71
- files.forEach((f) => {
72
- const icon = f.filePath.includes('.test.') || f.filePath.includes('__tests__') ? '🧪' : '📄';
85
+ files.forEach(f => {
86
+ const icon =
87
+ f.filePath.includes('.test.') || f.filePath.includes('__tests__') ? '🧪' : '📄';
73
88
  console.log(` ${icon} ${f.filePath}`);
74
89
  });
75
90
  }
@@ -195,28 +210,79 @@ ${body}
195
210
 
196
211
  _parseResponse(response) {
197
212
  try {
198
- // JSON 陣列
199
- const match = response.match(/\[[\s\S]*\]/);
200
- if (!match) {
213
+ // 1. 先去掉 markdown code fence(```json ... ``` 或 ``` ... ```)
214
+ const stripped = response
215
+ .replace(/```json\s*/gi, '')
216
+ .replace(/```\s*/g, '')
217
+ .trim();
218
+
219
+ // 2. 嘗試直接 parse 整個回應(AI 可能真的只回傳 JSON)
220
+ try {
221
+ const direct = JSON.parse(stripped);
222
+ if (Array.isArray(direct)) return this._normalizeFiles(direct);
223
+ } catch (_) {
224
+ // 繼續下一步
225
+ }
226
+
227
+ // 3. 用括號計數找出最外層完整 JSON 陣列(比貪婪 regex 更可靠)
228
+ const start = stripped.indexOf('[');
229
+ if (start === -1) {
201
230
  console.warn(' ⚠️ 無法從 AI 回應找到 JSON 陣列');
231
+ if (process.env.AUTODEV_DEBUG) {
232
+ console.log(' --- AI 原始回應(前 500 字)---');
233
+ console.log(response.slice(0, 500));
234
+ }
202
235
  return [];
203
236
  }
204
237
 
205
- const files = JSON.parse(match[0]);
238
+ let depth = 0;
239
+ let inString = false;
240
+ let escape = false;
241
+ let end = -1;
242
+
243
+ for (let i = start; i < stripped.length; i++) {
244
+ const ch = stripped[i];
245
+ if (escape) { escape = false; continue; }
246
+ if (ch === '\\' && inString) { escape = true; continue; }
247
+ if (ch === '"') { inString = !inString; continue; }
248
+ if (inString) continue;
249
+ if (ch === '[') depth++;
250
+ else if (ch === ']') {
251
+ depth--;
252
+ if (depth === 0) { end = i; break; }
253
+ }
254
+ }
255
+
256
+ if (end === -1) {
257
+ console.warn(' ⚠️ AI 回應中的 JSON 陣列括號不匹配');
258
+ return [];
259
+ }
260
+
261
+ const jsonStr = stripped.slice(start, end + 1);
262
+ const files = JSON.parse(jsonStr);
263
+
206
264
  if (!Array.isArray(files)) {
207
265
  console.warn(' ⚠️ AI 回應不是陣列格式');
208
266
  return [];
209
267
  }
210
268
 
211
- return files.map((f) => ({
212
- filePath: f.filePath || '',
213
- content: f.content || '',
214
- type: f.type || 'new',
215
- reason: f.reason || null,
216
- }));
269
+ return this._normalizeFiles(files);
217
270
  } catch (error) {
218
271
  console.warn(` ⚠️ 解析 AI 回應失敗:${error.message}`);
272
+ if (process.env.AUTODEV_DEBUG) {
273
+ console.log(' --- AI 原始回應(前 800 字)---');
274
+ console.log(response.slice(0, 800));
275
+ }
276
+ console.warn(' 💡 設定環境變數 AUTODEV_DEBUG=1 可查看 AI 完整回應內容');
219
277
  return [];
220
278
  }
221
279
  }
222
- }
280
+
281
+ _normalizeFiles(files) {
282
+ return files.map(f => ({
283
+ filePath: f.filePath || '',
284
+ content: f.content || '',
285
+ type: f.type || 'new',
286
+ reason: f.reason || null,
287
+ }));
288
+ }}
@@ -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,27 @@ 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
+ const projectRoot = this.config.projectRoot || process.cwd();
137
+ console.log('\n⏸️ 無測試可執行,已停止自動 commit / PR');
138
+ console.log('\n 最常見原因:測試檔案引用的套件尚未安裝');
139
+ console.log(` 👉 請先在目標專案目錄安裝依賴:`);
140
+ console.log(` cd ${projectRoot}`);
141
+ console.log(` npm install`);
142
+ console.log(` npx jest --listTests # 確認測試檔案是否被偵測到`);
143
+ console.log('\n 其他可能原因:');
144
+ console.log(' 2. 生成的測試語法有誤(Jest 無法解析)');
145
+ console.log(' 3. testPaths 設定未涵蓋新生成的測試路徑');
146
+ console.log('\n 安裝依賴並確認後,重新執行:');
147
+ console.log(' ai autodev <issue>');
148
+ console.log('\n 若確定要跳過測試直接 commit/PR:');
149
+ console.log(' ai autodev <issue> --skip-tests');
150
+ return;
151
+ }
133
152
  }
134
153
 
135
154
  // ── 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