ai-git-tools 2.0.67 → 2.0.69
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 +3 -83
- package/package.json +1 -1
- package/src/commands/dev-from-issue.js +0 -4
- package/src/commands/init.js +1 -0
- package/src/pr-modules/ai/pr-reviewer.js +156 -0
- package/src/pr-modules/core/config-loader.js +7 -1
- package/src/pr-modules/core/workflow.js +25 -0
- package/src/commands/auto-dev.js +0 -256
- package/src/commands/generate-code.js +0 -115
- package/src/commands/plan-issue.js +0 -91
- package/src/commands/write-and-test.js +0 -422
- package/src/core/test-generator.js +0 -165
- package/src/core/test-runner.js +0 -132
package/bin/cli.js
CHANGED
|
@@ -15,11 +15,7 @@ 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 { planIssueCommand } from '../src/commands/plan-issue.js';
|
|
19
|
-
import { generateCodeCommand } from '../src/commands/generate-code.js';
|
|
20
18
|
import { devFromIssueCommand } from '../src/commands/dev-from-issue.js';
|
|
21
|
-
import { writeAndTestCommand } from '../src/commands/write-and-test.js';
|
|
22
|
-
import { autoDevCommand } from '../src/commands/auto-dev.js';
|
|
23
19
|
|
|
24
20
|
// 讀取 package.json 獲取版本號
|
|
25
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -92,6 +88,7 @@ program
|
|
|
92
88
|
.option('--no-confirm', '跳過確認直接創建')
|
|
93
89
|
.option('--auto-labels', '自動添加 Labels (預設啟用)')
|
|
94
90
|
.option('--include-impact', '在 PR 中包含影響範圍分析和注意事項 (預設關閉)')
|
|
91
|
+
.option('--auto-review', 'PR 建立後發佈 AI 審查 comment')
|
|
95
92
|
.option('--force-new', '強制創建新 PR,不更新現有 PR')
|
|
96
93
|
.action(async (options) => {
|
|
97
94
|
try {
|
|
@@ -102,46 +99,11 @@ program
|
|
|
102
99
|
}
|
|
103
100
|
});
|
|
104
101
|
|
|
105
|
-
// Plan Issue
|
|
106
|
-
program
|
|
107
|
-
.command('plan-issue')
|
|
108
|
-
.description('AI 讀取 GitHub Issue 並生成結構化的實現計畫')
|
|
109
|
-
.requiredOption('--number <number>', 'Issue 編號')
|
|
110
|
-
.option('--no-confirm', '跳過確認,自動返回計畫')
|
|
111
|
-
.option('--model <model>', '指定 AI 模型')
|
|
112
|
-
.option('-v, --verbose', '顯示 Issue 詳細內容')
|
|
113
|
-
.action(async (options) => {
|
|
114
|
-
try {
|
|
115
|
-
await planIssueCommand(options);
|
|
116
|
-
process.exit(0);
|
|
117
|
-
} catch (error) {
|
|
118
|
-
process.exit(1);
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// Generate Code 命令
|
|
123
|
-
program
|
|
124
|
-
.command('generate-code')
|
|
125
|
-
.description('AI 依據 GitHub Issue 生成符合專案風格的代碼')
|
|
126
|
-
.requiredOption('--issue <number>', 'Issue 編號')
|
|
127
|
-
.requiredOption('--file <path>', '目標檔案路徑(不可已存在)')
|
|
128
|
-
.option('--context <description>', '額外說明或補充需求')
|
|
129
|
-
.option('--max-lines <number>', '最大行數限制(預設 500)', '500')
|
|
130
|
-
.option('--no-confirm', '跳過預覽確認,直接寫入')
|
|
131
|
-
.option('--model <model>', '指定 AI 模型')
|
|
132
|
-
.action(async (options) => {
|
|
133
|
-
try {
|
|
134
|
-
await generateCodeCommand(options);
|
|
135
|
-
process.exit(0);
|
|
136
|
-
} catch (error) {
|
|
137
|
-
process.exit(1);
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
|
|
102
|
+
// Plan Issue 命令(已废棄,功能已併入 dev-from-issue)
|
|
141
103
|
// Dev From Issue 命令(plan-issue + generate-code 合體)
|
|
142
104
|
program
|
|
143
105
|
.command('dev-from-issue')
|
|
144
|
-
.description('AI 讀取 Issue → 生成計畫 →
|
|
106
|
+
.description('AI 讀取 Issue → 生成計畫 → 生成代碼')
|
|
145
107
|
.requiredOption('--issue <number>', 'GitHub Issue 編號')
|
|
146
108
|
.option('--file <path>', '目標檔案路徑(若無則自動推斷)')
|
|
147
109
|
.option('--context <description>', '額外說明或補充需求')
|
|
@@ -157,46 +119,4 @@ program
|
|
|
157
119
|
}
|
|
158
120
|
});
|
|
159
121
|
|
|
160
|
-
// Write And Test 命令
|
|
161
|
-
program
|
|
162
|
-
.command('write-and-test')
|
|
163
|
-
.description('AI 為檔案生成測試(單元/元件),自動執行與修復,完成後發報告到 Issue(將自動讀取 dev-from-issue 的結果)')
|
|
164
|
-
.option('--file <path>', '原始碼路徑(對沒特別指定則自動從上次 dev-from-issue 讀取)')
|
|
165
|
-
.option('--issue <number>', '完成後發佈報告到此 Issue(對沒特別指定則自動從上次 dev-from-issue 讀取)')
|
|
166
|
-
.option('--test-type <type>', '測試類型:auto、unit、component 或 both(可用逗號指定多個)', 'auto')
|
|
167
|
-
.option('--max-fixes <number>', '最大自動修復次數(預設 2)', '2')
|
|
168
|
-
.option('--no-confirm', '跳過確認直接生成並執行測試')
|
|
169
|
-
.option('--model <model>', '指定 AI 模型')
|
|
170
|
-
.action(async (options) => {
|
|
171
|
-
try {
|
|
172
|
-
await writeAndTestCommand(options);
|
|
173
|
-
process.exit(0);
|
|
174
|
-
} catch (error) {
|
|
175
|
-
console.error(`\n[錯誤] ${error.message}`);
|
|
176
|
-
process.exit(1);
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// Auto Dev 整合命令
|
|
181
|
-
program
|
|
182
|
-
.command('auto-dev')
|
|
183
|
-
.description('一鍵自動化:從 GitHub Issue 到代碼生成與測試修正')
|
|
184
|
-
.requiredOption('--issue <number>', 'GitHub Issue 編號')
|
|
185
|
-
.option('--file <path>', '目標檔案路徑(若無則自動推斷)')
|
|
186
|
-
.option('--context <description>', '額外說明或補充需求')
|
|
187
|
-
.option('--test-type <type>', '測試類型:auto、unit、component 或 both(預設 auto)', 'auto')
|
|
188
|
-
.option('--max-lines <number>', '最大行數限制(預設 500)', '500')
|
|
189
|
-
.option('--max-fixes <number>', '最大自動修復次數(預設 2)', '2')
|
|
190
|
-
.option('--no-confirm', '全自動模式,跳過所有確認')
|
|
191
|
-
.option('--model <model>', '指定 AI 模型')
|
|
192
|
-
.action(async (options) => {
|
|
193
|
-
try {
|
|
194
|
-
await autoDevCommand(options);
|
|
195
|
-
process.exit(0);
|
|
196
|
-
} catch (error) {
|
|
197
|
-
console.error(`\n[錯誤] ${error.message}`);
|
|
198
|
-
process.exit(1);
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
|
|
202
122
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dev-from-issue 命令
|
|
3
3
|
* 讀取 GitHub Issue → 生成實現計畫 → 生成代碼
|
|
4
|
-
* 相當於 plan-issue + generate-code 的合體,並引導你下一步執行 write-and-test
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
import { resolve, join } from 'path';
|
|
@@ -100,9 +99,6 @@ export async function devFromIssueCommand(options = {}) {
|
|
|
100
99
|
logger.section('✅ 開發完成');
|
|
101
100
|
console.log(`Issue:#${issue.number} ${issue.title}`);
|
|
102
101
|
console.log(`檔案 :${filePath}`);
|
|
103
|
-
console.log('');
|
|
104
|
-
console.log('💡 下一步:執行測試並產生報告');
|
|
105
|
-
console.log(' ai-git-tools write-and-test');
|
|
106
102
|
}
|
|
107
103
|
|
|
108
104
|
// ============================================================
|
package/src/commands/init.js
CHANGED
|
@@ -26,6 +26,7 @@ export default {
|
|
|
26
26
|
defaultBase: 'release', // PR 預設目標分支(使用 'release' 自動偵測最新 release 分支,如 release-2025-m11.1)
|
|
27
27
|
autoLabels: true, // 自動新增 Labels
|
|
28
28
|
includeImpactAnalysis: false, // 是否在 PR 中包含影響範圍分析和注意事項(使用 --include-impact 啟用)
|
|
29
|
+
autoReview: true, // PR 建立後自動發佈 AI 審查 comment
|
|
29
30
|
},
|
|
30
31
|
|
|
31
32
|
// Reviewers 相關配置
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { CopilotClient, approveAll } from '@github/copilot-sdk';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { writeFileSync, unlinkSync, existsSync } from 'fs';
|
|
4
|
+
import { CONSTANTS, PROJECT_SKILLS_CONTEXT } from '../utils/constants.js';
|
|
5
|
+
import { getSkillsSummaryForPrompt, log } from '../utils/helpers.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* PR 審查員 - 負責生成 AI code review 並發布為 PR comment
|
|
9
|
+
*/
|
|
10
|
+
export class PRReviewer {
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
this.model = config.model || 'gpt-4.1';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 建立 AI 客戶端
|
|
17
|
+
*/
|
|
18
|
+
async createClient() {
|
|
19
|
+
const client = new CopilotClient();
|
|
20
|
+
const session = await client.createSession({
|
|
21
|
+
model: this.model,
|
|
22
|
+
onPermissionRequest: approveAll,
|
|
23
|
+
});
|
|
24
|
+
return { client, session };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 生成 PR 審查報告
|
|
29
|
+
*/
|
|
30
|
+
async generateReview(diff, commits, changedFiles) {
|
|
31
|
+
const skillsSummary = getSkillsSummaryForPrompt(PROJECT_SKILLS_CONTEXT);
|
|
32
|
+
const prompt = this.buildReviewPrompt(diff, commits, changedFiles, skillsSummary);
|
|
33
|
+
|
|
34
|
+
const { client, session } = await this.createClient();
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const responsePromise = session.sendAndWait({ prompt });
|
|
38
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
39
|
+
setTimeout(() => reject(new Error('AI 請求超時 (60 秒)')), 60000);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const response = await Promise.race([responsePromise, timeoutPromise]);
|
|
43
|
+
const reviewContent = response?.data.content?.trim() || '';
|
|
44
|
+
|
|
45
|
+
if (!reviewContent) {
|
|
46
|
+
throw new Error('AI 未能生成審查報告');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return this.wrapReviewBody(reviewContent);
|
|
50
|
+
} finally {
|
|
51
|
+
try {
|
|
52
|
+
await client.stop();
|
|
53
|
+
} catch (e) {
|
|
54
|
+
// 忽略關閉錯誤
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 將審查報告發布為 PR comment
|
|
61
|
+
*/
|
|
62
|
+
async postReviewComment(prNumber, reviewBody) {
|
|
63
|
+
const bodyFile = '/tmp/pr-review-comment.md';
|
|
64
|
+
writeFileSync(bodyFile, reviewBody);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
execSync(`gh pr comment ${prNumber} --body-file "${bodyFile}"`, {
|
|
68
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
69
|
+
});
|
|
70
|
+
log.success(` AI 審查報告已發布至 PR #${prNumber}`);
|
|
71
|
+
} finally {
|
|
72
|
+
try {
|
|
73
|
+
if (existsSync(bodyFile)) unlinkSync(bodyFile);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// 忽略刪除錯誤
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 建立 review prompt
|
|
82
|
+
*/
|
|
83
|
+
buildReviewPrompt(diff, commits, changedFiles, skillsSummary) {
|
|
84
|
+
const fileList = changedFiles
|
|
85
|
+
.slice(0, CONSTANTS.MAX_FILES_IN_PROMPT)
|
|
86
|
+
.join('\n');
|
|
87
|
+
|
|
88
|
+
const truncatedDiff = diff.substring(0, CONSTANTS.MAX_DIFF_LENGTH);
|
|
89
|
+
const diffTruncated = diff.length > CONSTANTS.MAX_DIFF_LENGTH;
|
|
90
|
+
|
|
91
|
+
return `你是一位資深工程師,正在進行嚴格但友善的程式碼審查。
|
|
92
|
+
請根據以下程式碼變更,以繁體中文(台灣正體)輸出審查報告。
|
|
93
|
+
|
|
94
|
+
${skillsSummary}
|
|
95
|
+
|
|
96
|
+
**變更檔案**:
|
|
97
|
+
${fileList}
|
|
98
|
+
${changedFiles.length > CONSTANTS.MAX_FILES_IN_PROMPT ? `... 還有 ${changedFiles.length - CONSTANTS.MAX_FILES_IN_PROMPT} 個檔案` : ''}
|
|
99
|
+
|
|
100
|
+
**Commit 訊息**:
|
|
101
|
+
${commits.split('\n').slice(0, CONSTANTS.MAX_COMMITS_IN_PROMPT).join('\n')}
|
|
102
|
+
|
|
103
|
+
**程式碼變更(diff)**:
|
|
104
|
+
\`\`\`diff
|
|
105
|
+
${truncatedDiff}
|
|
106
|
+
${diffTruncated ? '\n... (內容過長已截斷)' : ''}
|
|
107
|
+
\`\`\`
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
**輸出格式**(直接輸出以下 Markdown,不要加任何引導語):
|
|
112
|
+
|
|
113
|
+
### 🔴 需要處理的問題
|
|
114
|
+
[列出明確的 bug、安全漏洞、錯誤處理缺失等必須修正的項目]
|
|
115
|
+
[格式:- **[類型]** \`檔案名稱\` — 具體說明問題]
|
|
116
|
+
[若無此類問題,填寫「無」]
|
|
117
|
+
|
|
118
|
+
### 🟡 建議改善項目
|
|
119
|
+
[列出效能問題、規範違反、可讀性問題等建議改善項目]
|
|
120
|
+
[格式:- **[類型]** \`檔案名稱\` — 具體說明問題與建議]
|
|
121
|
+
[若無此類問題,填寫「無」]
|
|
122
|
+
|
|
123
|
+
### ✅ 良好實踐
|
|
124
|
+
[列出本次變更中值得肯定的設計或實踐]
|
|
125
|
+
[若無特別亮點,填寫「程式碼結構清晰,無明顯問題」]
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
**分類說明**:
|
|
130
|
+
- 🔴 需要處理:security(安全)、bug(邏輯錯誤)、error-handling(缺少錯誤處理)、breaking(破壞性變更未說明)
|
|
131
|
+
- 🟡 建議改善:performance(效能)、naming(命名規範)、structure(檔案/架構問題)、best-practice(最佳實踐)、test(缺少測試)
|
|
132
|
+
- ✅ 良好實踐:列出做得好的地方,鼓勵正確行為
|
|
133
|
+
|
|
134
|
+
**審查準則**:
|
|
135
|
+
1. 具體指出問題,盡量包含檔案名稱
|
|
136
|
+
2. 每個問題只列一次,不重複
|
|
137
|
+
3. 若程式碼品質良好,不要強制填充問題
|
|
138
|
+
4. 不要在開頭加引導語,直接輸出 ### 開頭的 Markdown
|
|
139
|
+
5. 全部使用繁體中文(台灣正體)`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 包裝最終的 review body(加上頭尾)
|
|
144
|
+
*/
|
|
145
|
+
wrapReviewBody(reviewContent) {
|
|
146
|
+
const now = new Date().toISOString().slice(0, 10);
|
|
147
|
+
return `## 🤖 AI 自動審查報告
|
|
148
|
+
|
|
149
|
+
> ⚡ 審查模型:${this.model} | 審查日期:${now}
|
|
150
|
+
|
|
151
|
+
${reviewContent}
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
*此 review 由 AI 自動生成,僅供參考,請人工確認後合併*`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -15,6 +15,7 @@ export function parseCliArgs() {
|
|
|
15
15
|
interactiveReviewers: undefined,
|
|
16
16
|
autoLabels: null,
|
|
17
17
|
includeImpactAnalysis: null,
|
|
18
|
+
autoReview: null,
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -43,6 +44,9 @@ export function parseCliArgs() {
|
|
|
43
44
|
case '--include-impact':
|
|
44
45
|
config.includeImpactAnalysis = true;
|
|
45
46
|
break;
|
|
47
|
+
case '--auto-review':
|
|
48
|
+
config.autoReview = true;
|
|
49
|
+
break;
|
|
46
50
|
case '--help':
|
|
47
51
|
showHelp();
|
|
48
52
|
process.exit(0);
|
|
@@ -71,7 +75,7 @@ export async function loadConfig() {
|
|
|
71
75
|
// 使用內建預設值
|
|
72
76
|
config = {
|
|
73
77
|
ai: { model: 'gpt-4.1', maxDiffLength: 8000, maxRetries: 3 },
|
|
74
|
-
github: { defaultBase: 'release', autoLabels: true, includeImpactAnalysis: false },
|
|
78
|
+
github: { defaultBase: 'release', autoLabels: true, includeImpactAnalysis: false, autoReview: false },
|
|
75
79
|
reviewers: { interactiveReviewers: true, maxSuggested: 5, gitHistoryDepth: 20, excludeAuthors: [] },
|
|
76
80
|
output: { verbose: false, saveHistory: false },
|
|
77
81
|
};
|
|
@@ -85,6 +89,7 @@ export async function loadConfig() {
|
|
|
85
89
|
if (cliConfig.interactiveReviewers !== undefined) config.reviewers.interactiveReviewers = cliConfig.interactiveReviewers;
|
|
86
90
|
if (cliConfig.autoLabels !== null) config.github.autoLabels = cliConfig.autoLabels;
|
|
87
91
|
if (cliConfig.includeImpactAnalysis !== null) config.github.includeImpactAnalysis = cliConfig.includeImpactAnalysis;
|
|
92
|
+
if (cliConfig.autoReview !== null) config.github.autoReview = cliConfig.autoReview;
|
|
88
93
|
|
|
89
94
|
// 其他 CLI 參數直接加入 config
|
|
90
95
|
config.baseBranch = cliConfig.baseBranch;
|
|
@@ -110,6 +115,7 @@ function showHelp() {
|
|
|
110
115
|
--no-confirm 跳過確認直接創建
|
|
111
116
|
--interactive-reviewers 啟用互動式 reviewer 選擇 (預設啟用)
|
|
112
117
|
--auto-labels 自動添加 Labels (預設啟用)
|
|
118
|
+
--auto-review PR 建立後發佈 AI 審查 comment
|
|
113
119
|
--help 顯示此說明
|
|
114
120
|
|
|
115
121
|
範例:
|
|
@@ -4,6 +4,7 @@ import { GitOperations } from './git-operations.js';
|
|
|
4
4
|
import { GitHubAPI } from './github-api.js';
|
|
5
5
|
import { AIAnalyzer } from '../ai/code-analyzer.js';
|
|
6
6
|
import { LabelAnalyzer } from '../ai/label-analyzer.js';
|
|
7
|
+
import { PRReviewer } from '../ai/pr-reviewer.js';
|
|
7
8
|
import { ReviewerSelector } from '../reviewers/reviewer-selector.js';
|
|
8
9
|
import { Logger } from '../ui/logger.js';
|
|
9
10
|
import { PRError, log } from '../utils/helpers.js';
|
|
@@ -19,6 +20,7 @@ export class PRWorkflow {
|
|
|
19
20
|
this.github = new GitHubAPI(); // 自動從 git remote 偵測組織名稱
|
|
20
21
|
this.ai = new AIAnalyzer({ model: config.ai.model });
|
|
21
22
|
this.labelAnalyzer = new LabelAnalyzer();
|
|
23
|
+
this.reviewer = new PRReviewer({ model: config.ai.model });
|
|
22
24
|
this.reviewerSelector = new ReviewerSelector({
|
|
23
25
|
interactiveReviewers: config.reviewers.interactiveReviewers,
|
|
24
26
|
maxSuggested: config.reviewers.maxSuggested,
|
|
@@ -95,6 +97,16 @@ export class PRWorkflow {
|
|
|
95
97
|
}
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
// 11. AI 自動審查(如果啟用)
|
|
101
|
+
if (this.config.github.autoReview === true && prUrl) {
|
|
102
|
+
try {
|
|
103
|
+
const prNumber = prUrl.split('/').pop();
|
|
104
|
+
await this.autoReviewPR(prNumber, changeData);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
log.warning('AI 審查失敗,跳過: ' + error.message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
98
110
|
this.logger.success('完成!');
|
|
99
111
|
}
|
|
100
112
|
|
|
@@ -426,6 +438,19 @@ export class PRWorkflow {
|
|
|
426
438
|
});
|
|
427
439
|
}
|
|
428
440
|
|
|
441
|
+
/**
|
|
442
|
+
* AI 自動審查 PR
|
|
443
|
+
*/
|
|
444
|
+
async autoReviewPR(prNumber, changeData) {
|
|
445
|
+
log.step(`正在使用 AI 審查程式碼 (${this.config.ai.model})...\n`);
|
|
446
|
+
const reviewBody = await this.reviewer.generateReview(
|
|
447
|
+
changeData.diff,
|
|
448
|
+
changeData.commits,
|
|
449
|
+
changeData.changedFiles
|
|
450
|
+
);
|
|
451
|
+
await this.reviewer.postReviewComment(prNumber, reviewBody);
|
|
452
|
+
}
|
|
453
|
+
|
|
429
454
|
/**
|
|
430
455
|
* 添加 Labels
|
|
431
456
|
*/
|
package/src/commands/auto-dev.js
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* auto-dev 整合命令
|
|
3
|
-
* 將 plan-issue、generate-code、write-and-test 串連起來
|
|
4
|
-
* 支援全自動模式(--no-confirm)和分步互動模式
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { resolve } from 'path';
|
|
8
|
-
import inquirer from 'inquirer';
|
|
9
|
-
import { IssueReader } from '../core/issue-reader.js';
|
|
10
|
-
import { CodeGenerator } from '../core/code-generator.js';
|
|
11
|
-
import { AIClient } from '../core/ai-client.js';
|
|
12
|
-
import { writeAndTestCommand, TEST_TYPE_CHOICES, normalizeTestTypeSelection } from './write-and-test.js';
|
|
13
|
-
import { Logger } from '../utils/logger.js';
|
|
14
|
-
|
|
15
|
-
export async function autoDevCommand(options = {}) {
|
|
16
|
-
const logger = new Logger();
|
|
17
|
-
|
|
18
|
-
const issueNumber = options.issue;
|
|
19
|
-
if (!issueNumber) {
|
|
20
|
-
logger.error('請提供 Issue 編號(--issue <number>)');
|
|
21
|
-
throw new Error('缺少 Issue 編號');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const noConfirm = options.noConfirm;
|
|
25
|
-
let filePath = options.file;
|
|
26
|
-
|
|
27
|
-
logger.header('auto-dev 整合工作流');
|
|
28
|
-
if (noConfirm) {
|
|
29
|
-
console.log('模式:全自動(跳過所有確認)');
|
|
30
|
-
} else {
|
|
31
|
-
console.log('模式:分步互動(每步完成後詢問)');
|
|
32
|
-
}
|
|
33
|
-
console.log('');
|
|
34
|
-
|
|
35
|
-
// ============================================================
|
|
36
|
-
// 第一步:讀取 Issue
|
|
37
|
-
// ============================================================
|
|
38
|
-
logger.step(`正在讀取 Issue #${issueNumber}...`);
|
|
39
|
-
const issue = await IssueReader.readIssue(issueNumber);
|
|
40
|
-
logger.success(`Issue:${issue.title}`);
|
|
41
|
-
console.log(`作者:${issue.author} | URL:${issue.url}`);
|
|
42
|
-
console.log('');
|
|
43
|
-
|
|
44
|
-
// ============================================================
|
|
45
|
-
// 第二步:生成計畫
|
|
46
|
-
// ============================================================
|
|
47
|
-
logger.step('正在分析 Issue 並生成實現計畫...');
|
|
48
|
-
const planPrompt = buildPlanPrompt(issue);
|
|
49
|
-
const plan = await AIClient.sendAndWait(planPrompt, options.model);
|
|
50
|
-
logger.section('AI 生成的實現計畫');
|
|
51
|
-
console.log(plan);
|
|
52
|
-
console.log('');
|
|
53
|
-
|
|
54
|
-
// ============================================================
|
|
55
|
-
// 第三步:詢問是否繼續(若非全自動)
|
|
56
|
-
// ============================================================
|
|
57
|
-
if (!noConfirm) {
|
|
58
|
-
const { proceed } = await inquirer.prompt([{
|
|
59
|
-
type: 'confirm',
|
|
60
|
-
name: 'proceed',
|
|
61
|
-
message: '計畫確認無誤,是否繼續進行代碼生成?',
|
|
62
|
-
default: true,
|
|
63
|
-
}]);
|
|
64
|
-
|
|
65
|
-
if (!proceed) {
|
|
66
|
-
logger.info('已取消。');
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ============================================================
|
|
72
|
-
// 第四步:決定目標檔案路徑(優先自動推斷)
|
|
73
|
-
// ============================================================
|
|
74
|
-
if (!filePath) {
|
|
75
|
-
filePath = await inferTargetFilePath(issue, plan, options.model);
|
|
76
|
-
if (filePath) {
|
|
77
|
-
logger.info(`已自動推斷目標檔案:${filePath}`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (!filePath) {
|
|
82
|
-
if (noConfirm) {
|
|
83
|
-
logger.error('無法自動推斷目標檔案路徑,請改用 --file 明確指定。');
|
|
84
|
-
throw new Error('缺少可推斷的目標檔案路徑');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const { file } = await inquirer.prompt([{
|
|
88
|
-
type: 'input',
|
|
89
|
-
name: 'file',
|
|
90
|
-
message: '無法自動推斷,請輸入要生成的檔案路徑(例如:src/core/new-feature.js):',
|
|
91
|
-
validate: (input) => input.trim().length > 0 ? true : '路徑不能為空',
|
|
92
|
-
}]);
|
|
93
|
-
filePath = file;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const absoluteFilePath = resolve(process.cwd(), filePath);
|
|
97
|
-
console.log('');
|
|
98
|
-
|
|
99
|
-
// ============================================================
|
|
100
|
-
// 第五步:生成代碼
|
|
101
|
-
// ============================================================
|
|
102
|
-
logger.step(`正在生成代碼:${filePath}`);
|
|
103
|
-
let codeResult;
|
|
104
|
-
try {
|
|
105
|
-
codeResult = await CodeGenerator.generateFile(issue, absoluteFilePath, {
|
|
106
|
-
maxLines: parseInt(options.maxLines || '500', 10),
|
|
107
|
-
language: getLanguageFromFilePath(filePath),
|
|
108
|
-
extraContext: options.context || '',
|
|
109
|
-
model: options.model,
|
|
110
|
-
});
|
|
111
|
-
logger.success(`代碼已生成:${codeResult.linesCount} 行`);
|
|
112
|
-
} catch (error) {
|
|
113
|
-
logger.error(`代碼生成失敗:${error.message}`);
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
console.log('');
|
|
118
|
-
|
|
119
|
-
const selectedTestType = await resolveTestTypeForAutoDev({
|
|
120
|
-
noConfirm,
|
|
121
|
-
requestedTestType: options.testType,
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
console.log('');
|
|
125
|
-
|
|
126
|
-
// ============================================================
|
|
127
|
-
// 第六步:強制執行測試流程
|
|
128
|
-
// ============================================================
|
|
129
|
-
const testResult = await writeAndTestCommand({
|
|
130
|
-
file: filePath,
|
|
131
|
-
testType: selectedTestType,
|
|
132
|
-
maxFixes: options.maxFixes || '2',
|
|
133
|
-
model: options.model,
|
|
134
|
-
noConfirm: true,
|
|
135
|
-
skipCommit: true,
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
if (!testResult?.success) {
|
|
139
|
-
logger.error('測試流程未完成,自動化工作流中止。');
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
console.log('');
|
|
144
|
-
logger.section('✅ 自動化工作流完成');
|
|
145
|
-
console.log(`Issue #${issue.number}:${issue.title}`);
|
|
146
|
-
console.log(`生成檔案:${filePath}`);
|
|
147
|
-
testResult.testFilePaths.forEach((testFilePath) => {
|
|
148
|
-
console.log(`測試檔案:${testFilePath.replace(process.cwd(), '.')}`);
|
|
149
|
-
});
|
|
150
|
-
console.log(`測試類型:${testResult.testTypes.join('、')}`);
|
|
151
|
-
console.log('狀態:已完成代碼與測試流程,未自動 commit');
|
|
152
|
-
console.log(`合計行數:${codeResult.linesCount + ' (程式碼)'}`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* 建立計畫生成 prompt
|
|
157
|
-
*/
|
|
158
|
-
function buildPlanPrompt(issue) {
|
|
159
|
-
return `你是一位軟體架構師。請閱讀以下 GitHub Issue 並建立詳細的實現計畫。
|
|
160
|
-
|
|
161
|
-
## Issue #${issue.number}
|
|
162
|
-
標題:${issue.title}
|
|
163
|
-
|
|
164
|
-
描述:
|
|
165
|
-
${issue.body || '(無描述)'}
|
|
166
|
-
|
|
167
|
-
${issue.labels.length ? `標籤:${issue.labels.join(', ')}` : ''}
|
|
168
|
-
|
|
169
|
-
## 輸出要求
|
|
170
|
-
請輸出包含以下內容的實現計畫:
|
|
171
|
-
|
|
172
|
-
1. **摘要** - 一句話說明要做什麼
|
|
173
|
-
2. **需要新增/修改的檔案** - 列出所有相關檔案路徑和各自的職責
|
|
174
|
-
3. **實現步驟** - 有序的任務清單,說明每個步驟的目的
|
|
175
|
-
4. **注意事項** - 潛在風險或需要留意的事項(若有)
|
|
176
|
-
|
|
177
|
-
請使用繁體中文,格式清晰,簡潔有力。`.trim();
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* 優先以規則與計畫內容推斷最可能的目標檔案
|
|
182
|
-
*/
|
|
183
|
-
async function inferTargetFilePath(issue, plan, model) {
|
|
184
|
-
const planCandidates = extractFileCandidates(plan);
|
|
185
|
-
if (planCandidates.length === 1) {
|
|
186
|
-
return planCandidates[0];
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (planCandidates.length > 1) {
|
|
190
|
-
const preferredCandidate = planCandidates.find((candidate) => candidate.startsWith('src/'));
|
|
191
|
-
if (preferredCandidate) {
|
|
192
|
-
return preferredCandidate;
|
|
193
|
-
}
|
|
194
|
-
return planCandidates[0];
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const prompt = `請根據以下 GitHub Issue 與實作計畫,推斷「最主要的實作檔案路徑」。
|
|
198
|
-
|
|
199
|
-
## 規則
|
|
200
|
-
- 只回傳單一檔案路徑
|
|
201
|
-
- 優先回傳 src/ 底下的實作檔,不要回傳測試檔、文件檔、設定檔
|
|
202
|
-
- 副檔名限制為 .js、.ts、.jsx、.tsx、.mjs、.cjs
|
|
203
|
-
- 不要包含程式碼區塊、不要加說明
|
|
204
|
-
|
|
205
|
-
## Issue #${issue.number}
|
|
206
|
-
標題:${issue.title}
|
|
207
|
-
描述:
|
|
208
|
-
${issue.body || '(無描述)'}
|
|
209
|
-
|
|
210
|
-
## 實作計畫
|
|
211
|
-
${plan}`.trim();
|
|
212
|
-
|
|
213
|
-
const inferred = (await AIClient.sendAndWait(prompt, model)).trim().split(/\s+/)[0];
|
|
214
|
-
return isValidTargetPath(inferred) ? inferred : null;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function extractFileCandidates(plan) {
|
|
218
|
-
const matches = plan.match(/([A-Za-z0-9_./-]+\.(?:js|ts|jsx|tsx|mjs|cjs))/g) || [];
|
|
219
|
-
const uniqueMatches = [...new Set(matches.filter(isValidTargetPath))];
|
|
220
|
-
|
|
221
|
-
return uniqueMatches.filter((candidate) => {
|
|
222
|
-
const normalized = candidate.toLowerCase();
|
|
223
|
-
return !normalized.includes('__tests__/')
|
|
224
|
-
&& !normalized.includes('.test.')
|
|
225
|
-
&& !normalized.includes('.spec.')
|
|
226
|
-
&& !normalized.endsWith('.config.js');
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function isValidTargetPath(value) {
|
|
231
|
-
return typeof value === 'string'
|
|
232
|
-
&& /^[A-Za-z0-9_./-]+\.(js|ts|jsx|tsx|mjs|cjs)$/.test(value)
|
|
233
|
-
&& !value.startsWith('/');
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function getLanguageFromFilePath(filePath) {
|
|
237
|
-
return /\.(ts|tsx)$/.test(filePath) ? 'ts' : 'js';
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
async function resolveTestTypeForAutoDev({ noConfirm, requestedTestType }) {
|
|
241
|
-
const defaultTestType = normalizeTestTypeSelection(requestedTestType || 'auto')[0] || 'auto';
|
|
242
|
-
|
|
243
|
-
if (noConfirm) {
|
|
244
|
-
return requestedTestType || 'auto';
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const { testType } = await inquirer.prompt([{
|
|
248
|
-
type: 'list',
|
|
249
|
-
name: 'testType',
|
|
250
|
-
message: '請選擇這次要執行的測試方式:',
|
|
251
|
-
choices: TEST_TYPE_CHOICES,
|
|
252
|
-
default: defaultTestType === 'auto' ? 'both' : defaultTestType,
|
|
253
|
-
}]);
|
|
254
|
-
|
|
255
|
-
return testType;
|
|
256
|
-
}
|