ai-git-tools 2.0.47 → 2.0.49

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
@@ -15,7 +15,6 @@ import { commitCommand } from '../src/commands/commit.js';
15
15
  import { commitAllCommand } from '../src/commands/commit-all.js';
16
16
  import { prCommand } from '../src/commands/pr.js';
17
17
  import { initCommand } from '../src/commands/init.js';
18
- import { autodevCommand } from '../src/commands/autodev.js';
19
18
 
20
19
  // 讀取 package.json 獲取版本號
21
20
  const __filename = fileURLToPath(import.meta.url);
@@ -97,25 +96,4 @@ program
97
96
  }
98
97
  });
99
98
 
100
- // AutoDev 命令
101
- program
102
- .command('autodev <issue>')
103
- .description('全自動開發流程:Issue → 測試 → commit-all → PR')
104
- .option('--dry-run', '乾運行:顯示計劃但不執行')
105
- .option('-v, --verbose', '詳細輸出')
106
- .option('--framework <name>', '強制指定測試框架(jest / vitest / mocha)')
107
- .option('--skip-commit', '跳過自動 commit')
108
- .option('--skip-pr', '跳過自動 PR 建立')
109
- .option('--skip-all', '只執行測試並發佈評論')
110
- .option('--commit-only', '執行測試 + commit,不建立 PR')
111
- .option('--skip-tests', '無測試可執行時仍繼續 commit/PR(不強制停止)')
112
- .action(async (issue, options) => {
113
- try {
114
- await autodevCommand(issue, options);
115
- process.exit(0);
116
- } catch (error) {
117
- process.exit(1);
118
- }
119
- });
120
-
121
99
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-git-tools",
3
- "version": "2.0.47",
3
+ "version": "2.0.49",
4
4
  "description": "AI-powered Git automation tools for commit messages and PR generation",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -459,82 +459,3 @@ export async function commitAllCommand() {
459
459
  throw error;
460
460
  }
461
461
  }
462
-
463
- // ─────────────────────────────────────────────────────────────
464
- // 程序化 API(供 ai autodev 呼叫,不影響 CLI 行為)
465
- // ─────────────────────────────────────────────────────────────
466
-
467
- /**
468
- * 程序化執行 commit-all 邏輯,供其他模組呼叫
469
- * 不會呼叫 process.exit(),而是回傳結果物件
470
- *
471
- * @param {Object} [options]
472
- * @param {boolean} [options.verbose]
473
- * @returns {Promise<{ success: boolean, commitCount: number, message: string }>}
474
- */
475
- export async function commitAllProgrammatic(options = {}) {
476
- const logger = new Logger();
477
-
478
- try {
479
- const config = await loadCommitConfig();
480
- if (options.verbose) {
481
- config.output.verbose = true;
482
- }
483
-
484
- const changes = getAllChanges();
485
- if (changes.length === 0) {
486
- return { success: true, commitCount: 0, message: '沒有需要提交的變更' };
487
- }
488
-
489
- const groups = await analyzeAndGroupChanges(changes, config);
490
- if (!groups || groups.length === 0) {
491
- return { success: false, commitCount: 0, message: 'AI 分析失敗,無法產生分組' };
492
- }
493
-
494
- // 補入未分組的檔案
495
- const groupedIndices = new Set(groups.flatMap((g) => g.file_indices));
496
- const ungrouped = [...Array(changes.length).keys()].filter((i) => !groupedIndices.has(i));
497
- if (ungrouped.length > 0) {
498
- groups.push({
499
- group_name: '其他變更',
500
- commit_type: 'chore',
501
- commit_scope: 'misc',
502
- file_indices: ungrouped,
503
- description: '未能自動分類的其他變更',
504
- });
505
- }
506
-
507
- let successCount = 0;
508
- for (let i = 0; i < groups.length; i++) {
509
- const group = groups[i];
510
- const groupFiles = group.file_indices.map((index) => changes[index]);
511
- const ok = await commitGroup(group, groupFiles, config);
512
- if (ok) successCount++;
513
- }
514
-
515
- // 清理 staged
516
- try {
517
- execSync('git reset HEAD -- .', { stdio: 'ignore' });
518
- } catch (_) { /* 忽略 */ }
519
-
520
- if (successCount === 0) {
521
- return { success: false, commitCount: 0, message: '所有群組提交失敗' };
522
- }
523
-
524
- // 取得最新 commit hash
525
- let commitHash = '';
526
- try {
527
- commitHash = execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim();
528
- } catch (_) { /* 忽略 */ }
529
-
530
- return {
531
- success: true,
532
- commitCount: successCount,
533
- commitHash,
534
- message: `成功提交 ${successCount}/${groups.length} 個群組`,
535
- };
536
- } catch (error) {
537
- logger.error(`commitAllProgrammatic 失敗:${error.message}`);
538
- return { success: false, commitCount: 0, message: error.message };
539
- }
540
- }
@@ -62,66 +62,3 @@ export async function prCommand() {
62
62
  throw error;
63
63
  }
64
64
  }
65
-
66
- // ─────────────────────────────────────────────────────────────
67
- // 程序化 API(供 ai autodev 呼叫,不影響 CLI 行為)
68
- // ─────────────────────────────────────────────────────────────
69
-
70
- /**
71
- * 程序化執行 PR 建立流程,供其他模組呼叫
72
- * 不互動(noConfirm = true),直接建立 PR 並回傳結果
73
- *
74
- * @param {Object} [options]
75
- * @param {string} [options.base] - 目標分支
76
- * @param {string} [options.head] - 來源分支
77
- * @param {boolean} [options.verbose]
78
- * @param {Object} [options.testResults] - 注入測試結果到 PR 描述(保留供未來擴充)
79
- * @returns {Promise<{ success: boolean, prUrl: string|null, prNumber: number|null, message: string }>}
80
- */
81
- export async function prProgrammatic(options = {}) {
82
- const logger = new Logger();
83
-
84
- if (!checkGHAuth(logger)) {
85
- return { success: false, prUrl: null, prNumber: null, message: 'GitHub CLI 未登入' };
86
- }
87
-
88
- try {
89
- const config = await loadConfig();
90
-
91
- // 程序化呼叫:跳過互動確認
92
- config.noConfirm = true;
93
- if (options.base) config.baseBranch = options.base;
94
- if (options.head) config.headBranch = options.head;
95
- if (options.verbose) config.output.verbose = true;
96
-
97
- const workflow = new PRWorkflow(config);
98
-
99
- // 攔截 PRWorkflow 建立的 PR URL
100
- let prUrl = null;
101
- let prNumber = null;
102
- const originalCreatePR = workflow.createPR?.bind(workflow);
103
- if (originalCreatePR) {
104
- workflow.createPR = async (...args) => {
105
- const url = await originalCreatePR(...args);
106
- prUrl = url;
107
- if (url) {
108
- const match = url.match(/\/pull\/(\d+)/);
109
- if (match) prNumber = parseInt(match[1], 10);
110
- }
111
- return url;
112
- };
113
- }
114
-
115
- await workflow.execute();
116
-
117
- return {
118
- success: true,
119
- prUrl,
120
- prNumber,
121
- message: prUrl ? `PR 已建立:${prUrl}` : 'PR 執行完成',
122
- };
123
- } catch (error) {
124
- logger.error(`prProgrammatic 失敗:${error.message}`);
125
- return { success: false, prUrl: null, prNumber: null, message: error.message };
126
- }
127
- }
@@ -7,52 +7,39 @@ import { CopilotClient } from '@github/copilot-sdk';
7
7
 
8
8
  export class AIClient {
9
9
  /**
10
- * 發送 prompt 並等待回應(帶重試機制)
11
- *
12
- * @param {string} prompt
13
- * @param {string} model
14
- * @param {number} maxRetries
15
- * @param {number} timeout - 毫秒,會直接傳給 SDK 的 sendAndWait
10
+ * 發送 prompt 並等待回應(帶重試機制和超時保護)
16
11
  */
17
- static async sendAndWait(prompt, model = 'claude-sonnet-4.5', maxRetries = 3, timeout = 60000) {
12
+ static async sendAndWait(prompt, model = 'gpt-4.1', maxRetries = 3, timeout = 60000) {
18
13
  let lastError = null;
19
14
 
20
15
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
21
16
  const client = new CopilotClient();
22
- let session = null;
23
17
  try {
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));
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);
29
24
  });
30
25
 
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
- }
26
+ const response = await Promise.race([responsePromise, timeoutPromise]);
38
27
 
39
28
  const content = response?.data?.content || '';
40
- if (!content) {
41
- throw new Error('AI 回傳空內容');
42
- }
43
29
  return content.trim();
44
-
45
30
  } catch (error) {
46
31
  lastError = error;
47
32
  if (attempt < maxRetries) {
48
- console.log(`⚠️ AI 請求失敗,重試第 ${attempt}/${maxRetries} 次... (${error.message})`);
33
+ console.log(`⚠️ AI 請求失敗,重試第 ${attempt}/${maxRetries} 次...`);
49
34
  await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
50
35
  }
51
36
  } finally {
52
- if (session) {
53
- try { await session.destroy(); } catch (_e) { /* 忽略 */ }
37
+ // 確保每次都關閉 client,無論成功或失敗
38
+ try {
39
+ await client.stop();
40
+ } catch (e) {
41
+ // 忽略關閉錯誤
54
42
  }
55
- try { await client.stop(); } catch (_e) { /* 忽略 */ }
56
43
  }
57
44
  }
58
45
 
@@ -179,56 +179,3 @@ export async function loadPRConfig() {
179
179
 
180
180
  return config;
181
181
  }
182
-
183
- /**
184
- * 載入 AutoDev 配置
185
- * 讀取 .ai-git-config.js(或 .mjs)的 autodev 區塊
186
- * 所有欄位皆選填,未設定時使用合理預設值
187
- *
188
- * @returns {Promise<Object>}
189
- */
190
- export async function loadAutodevConfig() {
191
- const defaults = {
192
- framework: null, // null = 自動偵測
193
- testPaths: [], // 空 = 自動發現
194
- testTimeout: 300_000, // 5 分鐘
195
- skipCommit: false,
196
- skipPr: false,
197
- verbose: false,
198
- projectRoot: process.cwd(),
199
- // AI 設定(可被 autodev 子區塊或全域 ai 區塊覆蓋)
200
- aiModel: 'claude-sonnet-4.5',
201
- maxRetries: 3,
202
- };
203
-
204
- // 支援 .ai-git-config.js 和 .ai-git-config.mjs
205
- const candidates = [
206
- resolve(process.cwd(), '.ai-git-config.js'),
207
- resolve(process.cwd(), '.ai-git-config.mjs'),
208
- ];
209
-
210
- let userAutodev = {};
211
- let userAi = {};
212
- for (const configPath of candidates) {
213
- if (existsSync(configPath)) {
214
- try {
215
- const imported = await import(`file://${configPath}`);
216
- const root = imported.default || {};
217
- userAutodev = root.autodev || {};
218
- userAi = root.ai || {}; // 讀取全域 ai 區塊
219
- break;
220
- } catch (error) {
221
- console.warn(`⚠️ 載入 autodev 配置失敗: ${error.message}`);
222
- }
223
- }
224
- }
225
-
226
- return {
227
- ...defaults,
228
- // 全域 ai 區塊優先度低於 autodev 子區塊
229
- aiModel: userAutodev.aiModel ?? userAi.model ?? defaults.aiModel,
230
- maxRetries: userAutodev.maxRetries ?? userAi.maxRetries ?? defaults.maxRetries,
231
- // 其他 autodev 欄位
232
- ...userAutodev,
233
- };
234
- }
@@ -394,88 +394,4 @@ export class GitHubAPI {
394
394
  log.error('無法添加團隊 reviewers,請手動操作');
395
395
  }
396
396
  }
397
-
398
- // ─────────────────────────────────────────────────────────────
399
- // Issue 評論功能(供 ai autodev 使用)
400
- // ─────────────────────────────────────────────────────────────
401
-
402
- /**
403
- * 在 Issue 上發佈評論
404
- * @param {string} owner
405
- * @param {string} repo
406
- * @param {number} issueNumber
407
- * @param {string} body - Markdown 內容
408
- * @returns {{ id: number, url: string }}
409
- */
410
- postCommentOnIssue(owner, repo, issueNumber, body) {
411
- const tmpFile = `/tmp/ai-autodev-comment-${Date.now()}.json`;
412
- try {
413
- const payload = JSON.stringify({ body });
414
- writeFileSync(tmpFile, payload, 'utf-8');
415
- const result = execSync(
416
- `gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --input "${tmpFile}"`,
417
- { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
418
- );
419
- const data = JSON.parse(result);
420
- return { id: data.id, url: data.html_url };
421
- } finally {
422
- this.safeUnlink(tmpFile);
423
- }
424
- }
425
-
426
- /**
427
- * 更新已存在的 Issue 評論
428
- * @param {string} owner
429
- * @param {string} repo
430
- * @param {number} commentId
431
- * @param {string} body
432
- * @returns {{ id: number, url: string }}
433
- */
434
- editIssueComment(owner, repo, commentId, body) {
435
- const tmpFile = `/tmp/ai-autodev-edit-${Date.now()}.json`;
436
- try {
437
- const payload = JSON.stringify({ body });
438
- writeFileSync(tmpFile, payload, 'utf-8');
439
- const result = execSync(
440
- `gh api repos/${owner}/${repo}/issues/comments/${commentId} --input "${tmpFile}" -X PATCH`,
441
- { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
442
- );
443
- const data = JSON.parse(result);
444
- return { id: data.id, url: data.html_url };
445
- } finally {
446
- this.safeUnlink(tmpFile);
447
- }
448
- }
449
-
450
- /**
451
- * 尋找 Issue 上含有特定 marker 的評論(用於重複執行時更新而非重建)
452
- * @param {string} owner
453
- * @param {string} repo
454
- * @param {number} issueNumber
455
- * @param {string} marker - 識別字串
456
- * @returns {{ id: number }|null}
457
- */
458
- findCommentWithMarker(owner, repo, issueNumber, marker) {
459
- try {
460
- const result = execSync(
461
- `gh api repos/${owner}/${repo}/issues/${issueNumber}/comments --jq '.[] | {id, body}'`,
462
- { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
463
- );
464
- // 每行一個 JSON 物件
465
- const lines = result.trim().split('\n').filter(Boolean);
466
- for (const line of lines) {
467
- try {
468
- const obj = JSON.parse(line);
469
- if (obj.body && obj.body.includes(marker)) {
470
- return { id: obj.id };
471
- }
472
- } catch (_) {
473
- // 忽略解析錯誤
474
- }
475
- }
476
- return null;
477
- } catch (_) {
478
- return null;
479
- }
480
- }
481
397
  }
@@ -1,62 +0,0 @@
1
- /**
2
- * AutoDev 命令
3
- * 完整自動化開發流程:Issue 解析 → 測試 → 發佈評論 → commit-all → PR
4
- *
5
- * 使用方式:
6
- * ai autodev <issue>
7
- * ai autodev 123
8
- * ai autodev https://github.com/org/repo/issues/123
9
- * ai autodev 123 --dry-run
10
- * ai autodev 123 --skip-pr
11
- */
12
-
13
- import { AutodevWorkflow } from '../dev-modules/core/autodev-workflow.js';
14
- import { loadAutodevConfig } from '../core/config-loader.js';
15
- import { handleError } from '../utils/helpers.js';
16
-
17
- /**
18
- * AutoDev 命令主函數
19
- * @param {string|number} issueInput - Issue 編號或 URL
20
- * @param {Object} cliOptions - 來自 commander 的選項
21
- */
22
- export async function autodevCommand(issueInput, cliOptions = {}) {
23
- if (!issueInput) {
24
- console.error('❌ 請提供 Issue 編號或 URL');
25
- console.error(' 用法:ai autodev <issue>');
26
- console.error(' 範例:ai autodev 123');
27
- process.exit(1);
28
- }
29
-
30
- try {
31
- // 載入設定
32
- const config = await loadAutodevConfig();
33
-
34
- // 合併 CLI 選項(CLI 優先)
35
- const options = {
36
- dryRun: cliOptions.dryRun ?? false,
37
- verbose: cliOptions.verbose ?? config.verbose ?? false,
38
- framework: cliOptions.framework ?? config.framework ?? null,
39
- skipCommit: cliOptions.skipCommit ?? config.skipCommit ?? false,
40
- skipPr: cliOptions.skipPr ?? config.skipPr ?? false,
41
- skipAll: cliOptions.skipAll ?? false,
42
- commitOnly: cliOptions.commitOnly ?? false,
43
- skipTests: cliOptions.skipTests ?? config.skipTests ?? false,
44
- };
45
-
46
- // 永遠顯示啟動設定,方便確認
47
- console.log('📋 autodev 啟動設定:');
48
- console.log(` AI 模型 : ${config.aiModel}`);
49
- console.log(` 重試次數 : ${config.maxRetries}`);
50
- console.log(` 測試框架 : ${options.framework ?? '自動偵測'}`);
51
- console.log(` Commit : ${options.skipAll || options.skipCommit ? '跳過' : '自動'}`);
52
- console.log(` PR : ${options.skipAll || options.skipPr || options.commitOnly ? '跳過' : '自動'}`);
53
- console.log(` 跳過測試 : ${options.skipTests ? '是(--skip-tests)' : '否(0 個測試時停止)'}`);
54
- if (options.dryRun) console.log(' ⚠️ 乾運行模式(不實際執行)');
55
-
56
- const workflow = new AutodevWorkflow(config);
57
- await workflow.execute(issueInput, options);
58
- } catch (error) {
59
- handleError(error);
60
- throw error;
61
- }
62
- }