ai-git-tools 2.0.40 → 2.0.42
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
|
@@ -196,6 +196,9 @@ export async function loadAutodevConfig() {
|
|
|
196
196
|
skipPr: false,
|
|
197
197
|
verbose: false,
|
|
198
198
|
projectRoot: process.cwd(),
|
|
199
|
+
// AI 設定(可被 autodev 子區塊或全域 ai 區塊覆蓋)
|
|
200
|
+
aiModel: 'gpt-4.1',
|
|
201
|
+
maxRetries: 3,
|
|
199
202
|
};
|
|
200
203
|
|
|
201
204
|
// 支援 .ai-git-config.js 和 .ai-git-config.mjs
|
|
@@ -205,11 +208,14 @@ export async function loadAutodevConfig() {
|
|
|
205
208
|
];
|
|
206
209
|
|
|
207
210
|
let userAutodev = {};
|
|
211
|
+
let userAi = {};
|
|
208
212
|
for (const configPath of candidates) {
|
|
209
213
|
if (existsSync(configPath)) {
|
|
210
214
|
try {
|
|
211
215
|
const imported = await import(`file://${configPath}`);
|
|
212
|
-
|
|
216
|
+
const root = imported.default || {};
|
|
217
|
+
userAutodev = root.autodev || {};
|
|
218
|
+
userAi = root.ai || {}; // 讀取全域 ai 區塊
|
|
213
219
|
break;
|
|
214
220
|
} catch (error) {
|
|
215
221
|
console.warn(`⚠️ 載入 autodev 配置失敗: ${error.message}`);
|
|
@@ -219,6 +225,10 @@ export async function loadAutodevConfig() {
|
|
|
219
225
|
|
|
220
226
|
return {
|
|
221
227
|
...defaults,
|
|
228
|
+
// 全域 ai 區塊優先度低於 autodev 子區塊
|
|
229
|
+
aiModel: userAutodev.aiModel ?? userAi.model ?? defaults.aiModel,
|
|
230
|
+
maxRetries: userAutodev.maxRetries ?? userAi.maxRetries ?? defaults.maxRetries,
|
|
231
|
+
// 其他 autodev 欄位
|
|
222
232
|
...userAutodev,
|
|
223
233
|
};
|
|
224
234
|
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI 代碼生成器
|
|
3
|
+
* 基於 Issue 描述,調用 AI 生成代碼框架/實現
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AIClient } from '../../core/ai-client.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} GeneratedCode
|
|
10
|
+
* @property {string} filePath - 要建立/修改的檔案路徑
|
|
11
|
+
* @property {string} content - 代碼內容
|
|
12
|
+
* @property {string} type - 'new' | 'modify' | 'skip'
|
|
13
|
+
* @property {string|null} reason - 若 skip 時的原因
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export class CodeGenerator {
|
|
17
|
+
constructor(config = {}) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.model = config.aiModel || 'gpt-4.1';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 基於 Issue 生成代碼檔案
|
|
24
|
+
* @param {import('../core/issue-parser.js').IssueData} issueData
|
|
25
|
+
* @param {string} [projectRoot] - 專案根目錄
|
|
26
|
+
* @returns {Promise<GeneratedCode[]>}
|
|
27
|
+
*/
|
|
28
|
+
async generateCodeFiles(issueData, projectRoot = process.cwd()) {
|
|
29
|
+
const prompt = this._buildPrompt(issueData, projectRoot);
|
|
30
|
+
|
|
31
|
+
console.log(' 🤖 調用 AI 生成代碼框架...');
|
|
32
|
+
|
|
33
|
+
let response;
|
|
34
|
+
try {
|
|
35
|
+
response = await AIClient.sendAndWait(
|
|
36
|
+
prompt,
|
|
37
|
+
this.model,
|
|
38
|
+
this.config.maxRetries || 3
|
|
39
|
+
);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.warn(` ⚠️ AI 調用失敗:${error.message}`);
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 解析 AI 回應
|
|
46
|
+
const files = this._parseResponse(response);
|
|
47
|
+
console.log(` 生成 ${files.length} 個檔案`);
|
|
48
|
+
return files;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 寫入生成的檔案到磁盤
|
|
53
|
+
* @param {GeneratedCode[]} files
|
|
54
|
+
* @param {string} projectRoot
|
|
55
|
+
* @returns {Promise<{ written: string[], skipped: string[] }>}
|
|
56
|
+
*/
|
|
57
|
+
async writeFiles(files, projectRoot = process.cwd()) {
|
|
58
|
+
const { writeFileSync, mkdirSync, existsSync } = await import('fs');
|
|
59
|
+
const { dirname, resolve } = await import('path');
|
|
60
|
+
|
|
61
|
+
const written = [];
|
|
62
|
+
const skipped = [];
|
|
63
|
+
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
if (file.type === 'skip') {
|
|
66
|
+
skipped.push(`${file.filePath} (${file.reason})`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const fullPath = resolve(projectRoot, file.filePath);
|
|
71
|
+
const dir = dirname(fullPath);
|
|
72
|
+
|
|
73
|
+
// 建立目錄
|
|
74
|
+
try {
|
|
75
|
+
if (!existsSync(dir)) {
|
|
76
|
+
mkdirSync(dir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.warn(` ⚠️ 無法建立目錄 ${dir}`);
|
|
80
|
+
skipped.push(file.filePath);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 寫入檔案
|
|
85
|
+
try {
|
|
86
|
+
writeFileSync(fullPath, file.content, 'utf-8');
|
|
87
|
+
written.push(file.filePath);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
console.warn(` ⚠️ 無法寫入 ${file.filePath}`);
|
|
90
|
+
skipped.push(file.filePath);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { written, skipped };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── 私有輔助方法
|
|
98
|
+
|
|
99
|
+
_buildPrompt(issueData, projectRoot) {
|
|
100
|
+
const { title, body, labels } = issueData;
|
|
101
|
+
|
|
102
|
+
return `你是一個資深軟體工程師,負責基於 GitHub Issue 生成完整的代碼框架和測試用例。
|
|
103
|
+
|
|
104
|
+
## Issue 資訊
|
|
105
|
+
標題:${title}
|
|
106
|
+
標籤:${labels.join(', ') || '無'}
|
|
107
|
+
描述:
|
|
108
|
+
${body}
|
|
109
|
+
|
|
110
|
+
## 任務
|
|
111
|
+
1. 分析上方 Issue 的需求
|
|
112
|
+
2. **同時**設計兩套檔案:
|
|
113
|
+
a. 實現檔案(src/ 中的原始碼)
|
|
114
|
+
b. 測試檔案(tests/ 或 __tests__/ 中的測試)
|
|
115
|
+
3. 生成初始實現和初始測試
|
|
116
|
+
|
|
117
|
+
## 重要:測試檔案生成
|
|
118
|
+
- 對每個實現檔案,都生成對應的測試檔案
|
|
119
|
+
- 測試檔案應放在 \`tests/\` 或 \`src/__tests__/\` 目錄
|
|
120
|
+
- 測試應覆蓋主要功能(可使用 TODO 標記待補充的測試用例)
|
|
121
|
+
- 測試檔案命名:實現檔案 \`.js\` → 測試檔案 \`.test.js\`
|
|
122
|
+
- 例:\`src/utils/helper.js\` → \`tests/utils/helper.test.js\`
|
|
123
|
+
|
|
124
|
+
## 輸出格式
|
|
125
|
+
請回傳 JSON 陣列,包含**源文件和測試文件**:
|
|
126
|
+
\`\`\`json
|
|
127
|
+
[
|
|
128
|
+
{
|
|
129
|
+
"filePath": "src/components/MyComponent.jsx",
|
|
130
|
+
"content": "// React 元件實現...",
|
|
131
|
+
"type": "new"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"filePath": "tests/components/MyComponent.test.jsx",
|
|
135
|
+
"content": "// Jest 測試...",
|
|
136
|
+
"type": "new"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"filePath": "src/utils/helper.js",
|
|
140
|
+
"content": "// 工具函數...",
|
|
141
|
+
"type": "new"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"filePath": "tests/utils/helper.test.js",
|
|
145
|
+
"content": "// 單元測試...",
|
|
146
|
+
"type": "new"
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
\`\`\`
|
|
150
|
+
|
|
151
|
+
只回傳 JSON,不要其他文字。
|
|
152
|
+
|
|
153
|
+
## 測試框架指南(假設 Jest)
|
|
154
|
+
- \`describe('名稱', () => { ... })\` 分組
|
|
155
|
+
- \`test('應該...', () => { ... })\` 單一測試
|
|
156
|
+
- 包含 arrange-act-assert 結構
|
|
157
|
+
- 對 TODO 部分用 \`test.todo('待實現')\`
|
|
158
|
+
- 可包含 \`// TODO: 補充邊界情況\` 註釋
|
|
159
|
+
|
|
160
|
+
專案根目錄:${projectRoot}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
_parseResponse(response) {
|
|
164
|
+
try {
|
|
165
|
+
// 找 JSON 陣列
|
|
166
|
+
const match = response.match(/\[[\s\S]*\]/);
|
|
167
|
+
if (!match) {
|
|
168
|
+
console.warn(' ⚠️ 無法從 AI 回應找到 JSON 陣列');
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const files = JSON.parse(match[0]);
|
|
173
|
+
if (!Array.isArray(files)) {
|
|
174
|
+
console.warn(' ⚠️ AI 回應不是陣列格式');
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return files.map((f) => ({
|
|
179
|
+
filePath: f.filePath || '',
|
|
180
|
+
content: f.content || '',
|
|
181
|
+
type: f.type || 'new',
|
|
182
|
+
reason: f.reason || null,
|
|
183
|
+
}));
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.warn(` ⚠️ 解析 AI 回應失敗:${error.message}`);
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -7,6 +7,7 @@ import { IssueParser } from './issue-parser.js';
|
|
|
7
7
|
import { TestDetector } from '../test/test-detector.js';
|
|
8
8
|
import { createExecutor } from '../test/executor-factory.js';
|
|
9
9
|
import { ResultFormatter, COMMENT_MARKER } from '../test/result-formatter.js';
|
|
10
|
+
import { CodeGenerator } from '../ai/code-generator.js';
|
|
10
11
|
import { GitHubAPI } from '../../pr-modules/core/github-api.js';
|
|
11
12
|
import { commitAllProgrammatic } from '../../commands/commit-all.js';
|
|
12
13
|
import { prProgrammatic } from '../../commands/pr.js';
|
|
@@ -31,6 +32,10 @@ export class AutodevWorkflow {
|
|
|
31
32
|
this.issueParser = new IssueParser();
|
|
32
33
|
this.testDetector = new TestDetector(config.projectRoot || process.cwd());
|
|
33
34
|
this.formatter = new ResultFormatter();
|
|
35
|
+
this.codeGenerator = new CodeGenerator({
|
|
36
|
+
aiModel: config.aiModel,
|
|
37
|
+
maxRetries: config.maxRetries,
|
|
38
|
+
});
|
|
34
39
|
this.github = new GitHubAPI();
|
|
35
40
|
}
|
|
36
41
|
|
|
@@ -50,11 +55,15 @@ export class AutodevWorkflow {
|
|
|
50
55
|
step(++stepIdx, TOTAL, '解析 Issue...');
|
|
51
56
|
const issueData = await this._parseIssue(issueInput, options);
|
|
52
57
|
|
|
53
|
-
// ── 2.
|
|
58
|
+
// ── 2. AI 自動開發(生成代碼框架)────────────────────────
|
|
59
|
+
step(++stepIdx, TOTAL, '生成代碼框架...');
|
|
60
|
+
const generatedFiles = await this._generateCode(issueData, options);
|
|
61
|
+
|
|
62
|
+
// ── 3. 偵測框架 ──────────────────────────────────────────
|
|
54
63
|
step(++stepIdx, TOTAL, '偵測測試框架...');
|
|
55
64
|
const framework = this._detectFramework(options);
|
|
56
65
|
|
|
57
|
-
// ──
|
|
66
|
+
// ── 4. 發現測試檔案 ──────────────────────────────────────
|
|
58
67
|
step(++stepIdx, TOTAL, '搜尋測試檔案...');
|
|
59
68
|
const testFiles = this.testDetector.discoverTests(framework, {
|
|
60
69
|
testPaths: this.config.testPaths,
|
|
@@ -63,11 +72,12 @@ export class AutodevWorkflow {
|
|
|
63
72
|
|
|
64
73
|
if (options.dryRun) {
|
|
65
74
|
console.log('\n🔍 乾運行模式:以下是執行計劃\n');
|
|
66
|
-
console.log(` Issue
|
|
67
|
-
console.log(`
|
|
68
|
-
console.log(`
|
|
69
|
-
console.log(`
|
|
70
|
-
console.log(`
|
|
75
|
+
console.log(` Issue : #${issueData.number} - ${issueData.title}`);
|
|
76
|
+
console.log(` 代碼檔案 : ${generatedFiles.length} 個將被建立`);
|
|
77
|
+
console.log(` 框架 : ${framework}`);
|
|
78
|
+
console.log(` 測試數量 : ${testFiles.length} 個檔案`);
|
|
79
|
+
console.log(` Commit : ${this._willCommit(options) ? '✅ 會執行' : '⏭️ 跳過'}`);
|
|
80
|
+
console.log(` PR : ${this._willCreatePR(options) ? '✅ 會執行' : '⏭️ 跳過'}`);
|
|
71
81
|
return;
|
|
72
82
|
}
|
|
73
83
|
|
|
@@ -75,7 +85,7 @@ export class AutodevWorkflow {
|
|
|
75
85
|
console.warn(' ⚠️ 未找到任何測試檔案,跳過測試步驟');
|
|
76
86
|
}
|
|
77
87
|
|
|
78
|
-
// ──
|
|
88
|
+
// ── 5. 執行測試 ──────────────────────────────────────────
|
|
79
89
|
step(++stepIdx, TOTAL, `執行 ${framework} 測試...`);
|
|
80
90
|
const executor = createExecutor(framework, {
|
|
81
91
|
timeout: this.config.testTimeout,
|
|
@@ -92,13 +102,15 @@ export class AutodevWorkflow {
|
|
|
92
102
|
` ${allPassed ? '✅' : '❌'} ${testResults.passed}/${testResults.total} 通過(${passRate}%)· ${(testResults.duration / 1000).toFixed(1)}s`
|
|
93
103
|
);
|
|
94
104
|
|
|
95
|
-
// ──
|
|
105
|
+
// ── 6. 格式化結果 ─────────────────────────────────────────
|
|
96
106
|
step(++stepIdx, TOTAL, '格式化測試結果...');
|
|
97
|
-
const meta = {
|
|
107
|
+
const meta = {
|
|
108
|
+
generatedFiles: generatedFiles.length,
|
|
109
|
+
};
|
|
98
110
|
let markdown = this.formatter.formatAsMarkdown(testResults, issueData, meta);
|
|
99
111
|
|
|
100
|
-
// ──
|
|
101
|
-
step(++stepIdx, TOTAL,
|
|
112
|
+
// ── 7. 發佈到 Issue 評論(初稿,無 commit/PR 資訊)─────────
|
|
113
|
+
step(++stepIdx, TOTAL, `發佈結果到 Issue #${issueData.number}...`);
|
|
102
114
|
const commentResult = await this._postOrUpdateComment(
|
|
103
115
|
issueData,
|
|
104
116
|
markdown,
|
|
@@ -108,7 +120,7 @@ export class AutodevWorkflow {
|
|
|
108
120
|
console.log(` 📝 評論已發佈:${commentResult.url}`);
|
|
109
121
|
}
|
|
110
122
|
|
|
111
|
-
// ──
|
|
123
|
+
// ── 8. 若測試失敗,停止 ───────────────────────────────────
|
|
112
124
|
if (!allPassed && testResults.total > 0) {
|
|
113
125
|
console.log('\n❌ 測試未全部通過,已停止自動 commit / PR');
|
|
114
126
|
console.log(' 修復失敗的測試後,重新執行 `ai autodev <issue>`');
|
|
@@ -120,7 +132,7 @@ export class AutodevWorkflow {
|
|
|
120
132
|
console.warn(' ⚠️ 無測試可執行,繼續後續流程');
|
|
121
133
|
}
|
|
122
134
|
|
|
123
|
-
// ──
|
|
135
|
+
// ── 9. 自動 commit-all ───────────────────────────────────
|
|
124
136
|
if (this._willCommit(options)) {
|
|
125
137
|
step(++stepIdx, TOTAL, '自動提交(ai commit-all)...');
|
|
126
138
|
const commitResult = await commitAllProgrammatic({ verbose: options.verbose });
|
|
@@ -133,7 +145,7 @@ export class AutodevWorkflow {
|
|
|
133
145
|
console.log(` ✅ ${commitResult.message}(${commitResult.commitHash})`);
|
|
134
146
|
}
|
|
135
147
|
|
|
136
|
-
// ──
|
|
148
|
+
// ── 10. 自動建立 PR ───────────────────────────────────────
|
|
137
149
|
if (this._willCreatePR(options)) {
|
|
138
150
|
step(++stepIdx, TOTAL, '自動建立 PR(ai pr)...');
|
|
139
151
|
const prResult = await prProgrammatic({ verbose: options.verbose });
|
|
@@ -154,11 +166,12 @@ export class AutodevWorkflow {
|
|
|
154
166
|
// ── 完成摘要 ──────────────────────────────────────────────
|
|
155
167
|
console.log('\n' + '═'.repeat(60));
|
|
156
168
|
console.log('🎉 ai autodev 完成!');
|
|
157
|
-
console.log(` Issue
|
|
158
|
-
console.log(`
|
|
159
|
-
|
|
160
|
-
if (meta.
|
|
161
|
-
if (
|
|
169
|
+
console.log(` Issue : #${issueData.number} - ${issueData.title}`);
|
|
170
|
+
if (generatedFiles.length > 0) console.log(` 代碼檔案 : ${generatedFiles.length} 個已建立`);
|
|
171
|
+
console.log(` 測試 : ${testResults.passed}/${testResults.total} 通過`);
|
|
172
|
+
if (meta.commitHash) console.log(` Commit : ${meta.commitHash}`);
|
|
173
|
+
if (meta.prUrl) console.log(` PR : ${meta.prUrl}`);
|
|
174
|
+
if (commentResult) console.log(` 評論 : ${commentResult.url}`);
|
|
162
175
|
console.log('═'.repeat(60));
|
|
163
176
|
}
|
|
164
177
|
|
|
@@ -173,6 +186,34 @@ export class AutodevWorkflow {
|
|
|
173
186
|
return issueData;
|
|
174
187
|
}
|
|
175
188
|
|
|
189
|
+
async _generateCode(issueData, options) {
|
|
190
|
+
try {
|
|
191
|
+
const projectRoot = this.config.projectRoot || process.cwd();
|
|
192
|
+
const files = await this.codeGenerator.generateCodeFiles(issueData, projectRoot);
|
|
193
|
+
|
|
194
|
+
if (files.length === 0) {
|
|
195
|
+
console.log(' ℹ️ 沒有需要生成的代碼檔案');
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 寫入檔案
|
|
200
|
+
const { written, skipped } = await this.codeGenerator.writeFiles(files, projectRoot);
|
|
201
|
+
console.log(` ✅ 已寫入 ${written.length} 個檔案`);
|
|
202
|
+
if (skipped.length > 0) {
|
|
203
|
+
console.warn(` ⚠️ 跳過 ${skipped.length} 個檔案`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (options.verbose) {
|
|
207
|
+
written.forEach((f) => console.log(` 新建: ${f}`));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return written;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.error(` ❌ 代碼生成失敗:${error.message}`);
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
176
217
|
_detectFramework(options) {
|
|
177
218
|
const forced = options.framework || this.config.framework;
|
|
178
219
|
if (forced) {
|
|
@@ -223,8 +264,8 @@ export class AutodevWorkflow {
|
|
|
223
264
|
}
|
|
224
265
|
|
|
225
266
|
_calcTotalSteps(options) {
|
|
226
|
-
// 基本步驟:Issue解析 + 框架偵測 + 測試發現 + 執行 + 格式化 + 發佈
|
|
227
|
-
let count =
|
|
267
|
+
// 基本步驟:Issue解析 + 代碼生成 + 框架偵測 + 測試發現 + 執行 + 格式化 + 發佈
|
|
268
|
+
let count = 7;
|
|
228
269
|
if (this._willCommit(options)) count++;
|
|
229
270
|
if (this._willCreatePR(options)) count++;
|
|
230
271
|
return count;
|