ai-git-tools 2.0.48 → 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 +0 -22
- package/package.json +1 -1
- package/src/commands/commit-all.js +0 -79
- package/src/commands/init.js +0 -38
- package/src/commands/pr.js +0 -63
- package/src/core/ai-client.js +15 -28
- package/src/core/config-loader.js +0 -53
- package/src/pr-modules/core/github-api.js +0 -84
- package/src/commands/autodev.js +0 -62
- package/src/dev-modules/ai/code-generator.js +0 -288
- package/src/dev-modules/core/autodev-workflow.js +0 -305
- package/src/dev-modules/core/issue-parser.js +0 -144
- package/src/dev-modules/test/executor-base.js +0 -74
- package/src/dev-modules/test/executor-factory.js +0 -29
- package/src/dev-modules/test/jest-executor.js +0 -107
- package/src/dev-modules/test/mocha-executor.js +0 -95
- package/src/dev-modules/test/result-formatter.js +0 -90
- package/src/dev-modules/test/test-detector.js +0 -170
- package/src/dev-modules/test/vitest-executor.js +0 -90
|
@@ -1,288 +0,0 @@
|
|
|
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
|
-
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
|
-
];
|
|
40
|
-
if (!VALID_MODELS.includes(this.model)) {
|
|
41
|
-
console.warn(` ⚠️ model「${this.model}」可能無效!支援的模型:${VALID_MODELS.join(', ')}`);
|
|
42
|
-
}
|
|
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
|
-
}
|
|
54
|
-
|
|
55
|
-
let response;
|
|
56
|
-
try {
|
|
57
|
-
response = await AIClient.sendAndWait(
|
|
58
|
-
prompt,
|
|
59
|
-
this.model,
|
|
60
|
-
this.config.maxRetries || 3,
|
|
61
|
-
timeout
|
|
62
|
-
);
|
|
63
|
-
} catch (error) {
|
|
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');
|
|
71
|
-
return [];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (!response) {
|
|
75
|
-
console.warn(' ⚠️ AI 回傳空回應');
|
|
76
|
-
return [];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// 解析 AI 回應
|
|
80
|
-
const files = this._parseResponse(response);
|
|
81
|
-
if (files.length === 0) {
|
|
82
|
-
console.warn(' ⚠️ AI 未回傳任何檔案(可能 JSON 格式有誤)');
|
|
83
|
-
} else {
|
|
84
|
-
console.log(` ✅ AI 規劃生成 ${files.length} 個檔案:`);
|
|
85
|
-
files.forEach(f => {
|
|
86
|
-
const icon =
|
|
87
|
-
f.filePath.includes('.test.') || f.filePath.includes('__tests__') ? '🧪' : '📄';
|
|
88
|
-
console.log(` ${icon} ${f.filePath}`);
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
return files;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* 寫入生成的檔案到磁盤
|
|
96
|
-
* @param {GeneratedCode[]} files
|
|
97
|
-
* @param {string} projectRoot
|
|
98
|
-
* @returns {Promise<{ written: string[], skipped: string[] }>}
|
|
99
|
-
*/
|
|
100
|
-
async writeFiles(files, projectRoot = process.cwd()) {
|
|
101
|
-
const { writeFileSync, mkdirSync, existsSync } = await import('fs');
|
|
102
|
-
const { dirname, resolve } = await import('path');
|
|
103
|
-
|
|
104
|
-
const written = [];
|
|
105
|
-
const skipped = [];
|
|
106
|
-
|
|
107
|
-
for (const file of files) {
|
|
108
|
-
if (file.type === 'skip') {
|
|
109
|
-
console.log(` ⏭️ 跳過 ${file.filePath}${file.reason ? ` (${file.reason})` : ''}`);
|
|
110
|
-
skipped.push(`${file.filePath}${file.reason ? ` (${file.reason})` : ''}`);
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const fullPath = resolve(projectRoot, file.filePath);
|
|
115
|
-
const dir = dirname(fullPath);
|
|
116
|
-
|
|
117
|
-
// 建立目錄
|
|
118
|
-
try {
|
|
119
|
-
if (!existsSync(dir)) {
|
|
120
|
-
mkdirSync(dir, { recursive: true });
|
|
121
|
-
}
|
|
122
|
-
} catch (e) {
|
|
123
|
-
console.warn(` ⚠️ 無法建立目錄 ${dir}:${e.message}`);
|
|
124
|
-
skipped.push(file.filePath);
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// 寫入檔案(即時顯示)
|
|
129
|
-
try {
|
|
130
|
-
const isTest = file.filePath.includes('.test.') || file.filePath.includes('__tests__');
|
|
131
|
-
const icon = isTest ? '🧪' : '📄';
|
|
132
|
-
const existed = existsSync(fullPath);
|
|
133
|
-
writeFileSync(fullPath, file.content, 'utf-8');
|
|
134
|
-
written.push(file.filePath);
|
|
135
|
-
console.log(` ${icon} ${existed ? '更新' : '新建'} ${file.filePath}`);
|
|
136
|
-
} catch (e) {
|
|
137
|
-
console.warn(` ⚠️ 無法寫入 ${file.filePath}:${e.message}`);
|
|
138
|
-
skipped.push(file.filePath);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return { written, skipped };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// ── 私有輔助方法
|
|
146
|
-
|
|
147
|
-
_buildPrompt(issueData, projectRoot) {
|
|
148
|
-
const { title, body, labels } = issueData;
|
|
149
|
-
|
|
150
|
-
return `你是一個資深軟體工程師,負責基於 GitHub Issue 生成完整的代碼框架和測試用例。
|
|
151
|
-
|
|
152
|
-
## Issue 資訊
|
|
153
|
-
標題:${title}
|
|
154
|
-
標籤:${labels.join(', ') || '無'}
|
|
155
|
-
描述:
|
|
156
|
-
${body}
|
|
157
|
-
|
|
158
|
-
## 任務
|
|
159
|
-
1. 分析上方 Issue 的需求
|
|
160
|
-
2. **同時**設計兩套檔案:
|
|
161
|
-
a. 實現檔案(src/ 中的原始碼)
|
|
162
|
-
b. 測試檔案(tests/ 或 __tests__/ 中的測試)
|
|
163
|
-
3. 生成初始實現和初始測試
|
|
164
|
-
|
|
165
|
-
## 重要:測試檔案生成
|
|
166
|
-
- 對每個實現檔案,都生成對應的測試檔案
|
|
167
|
-
- 測試檔案應放在 \`tests/\` 或 \`src/__tests__/\` 目錄
|
|
168
|
-
- 測試應覆蓋主要功能(可使用 TODO 標記待補充的測試用例)
|
|
169
|
-
- 測試檔案命名:實現檔案 \`.js\` → 測試檔案 \`.test.js\`
|
|
170
|
-
- 例:\`src/utils/helper.js\` → \`tests/utils/helper.test.js\`
|
|
171
|
-
|
|
172
|
-
## 輸出格式
|
|
173
|
-
請回傳 JSON 陣列,包含**源文件和測試文件**:
|
|
174
|
-
\`\`\`json
|
|
175
|
-
[
|
|
176
|
-
{
|
|
177
|
-
"filePath": "src/components/MyComponent.jsx",
|
|
178
|
-
"content": "// React 元件實現...",
|
|
179
|
-
"type": "new"
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
"filePath": "tests/components/MyComponent.test.jsx",
|
|
183
|
-
"content": "// Jest 測試...",
|
|
184
|
-
"type": "new"
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
"filePath": "src/utils/helper.js",
|
|
188
|
-
"content": "// 工具函數...",
|
|
189
|
-
"type": "new"
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
"filePath": "tests/utils/helper.test.js",
|
|
193
|
-
"content": "// 單元測試...",
|
|
194
|
-
"type": "new"
|
|
195
|
-
}
|
|
196
|
-
]
|
|
197
|
-
\`\`\`
|
|
198
|
-
|
|
199
|
-
只回傳 JSON,不要其他文字。
|
|
200
|
-
|
|
201
|
-
## 測試框架指南(假設 Jest)
|
|
202
|
-
- \`describe('名稱', () => { ... })\` 分組
|
|
203
|
-
- \`test('應該...', () => { ... })\` 單一測試
|
|
204
|
-
- 包含 arrange-act-assert 結構
|
|
205
|
-
- 對 TODO 部分用 \`test.todo('待實現')\`
|
|
206
|
-
- 可包含 \`// TODO: 補充邊界情況\` 註釋
|
|
207
|
-
|
|
208
|
-
專案根目錄:${projectRoot}`;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
_parseResponse(response) {
|
|
212
|
-
try {
|
|
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) {
|
|
230
|
-
console.warn(' ⚠️ 無法從 AI 回應找到 JSON 陣列');
|
|
231
|
-
if (process.env.AUTODEV_DEBUG) {
|
|
232
|
-
console.log(' --- AI 原始回應(前 500 字)---');
|
|
233
|
-
console.log(response.slice(0, 500));
|
|
234
|
-
}
|
|
235
|
-
return [];
|
|
236
|
-
}
|
|
237
|
-
|
|
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
|
-
|
|
264
|
-
if (!Array.isArray(files)) {
|
|
265
|
-
console.warn(' ⚠️ AI 回應不是陣列格式');
|
|
266
|
-
return [];
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return this._normalizeFiles(files);
|
|
270
|
-
} catch (error) {
|
|
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 完整回應內容');
|
|
277
|
-
return [];
|
|
278
|
-
}
|
|
279
|
-
}
|
|
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
|
-
}}
|
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AutoDev 工作流程編排
|
|
3
|
-
* 整合 Issue 解析 → 測試執行 → 結果發佈 → commit-all → PR
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { IssueParser } from './issue-parser.js';
|
|
7
|
-
import { TestDetector } from '../test/test-detector.js';
|
|
8
|
-
import { createExecutor } from '../test/executor-factory.js';
|
|
9
|
-
import { ResultFormatter, COMMENT_MARKER } from '../test/result-formatter.js';
|
|
10
|
-
import { CodeGenerator } from '../ai/code-generator.js';
|
|
11
|
-
import { GitHubAPI } from '../../pr-modules/core/github-api.js';
|
|
12
|
-
import { commitAllProgrammatic } from '../../commands/commit-all.js';
|
|
13
|
-
import { prProgrammatic } from '../../commands/pr.js';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @typedef {Object} AutodevOptions
|
|
17
|
-
* @property {boolean} [dryRun] - 乾運行:只顯示計劃,不執行
|
|
18
|
-
* @property {boolean} [verbose] - 詳細輸出
|
|
19
|
-
* @property {string} [framework] - 強制指定框架(覆蓋自動偵測)
|
|
20
|
-
* @property {boolean} [skipCommit] - 跳過 commit-all
|
|
21
|
-
* @property {boolean} [skipPr] - 跳過 PR 建立
|
|
22
|
-
* @property {boolean} [skipAll] - 只測試 + 發佈評論,跳過 commit 和 PR
|
|
23
|
-
* @property {boolean} [commitOnly] - 測試 + commit,不建立 PR
|
|
24
|
-
* @property {boolean} [skipTests] - 無測試可執行時繼續(而非停止)
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
export class AutodevWorkflow {
|
|
28
|
-
/**
|
|
29
|
-
* @param {Object} config - .ai-git-config.js 的 autodev 區塊
|
|
30
|
-
*/
|
|
31
|
-
constructor(config = {}) {
|
|
32
|
-
this.config = config;
|
|
33
|
-
this.issueParser = new IssueParser();
|
|
34
|
-
this.testDetector = new TestDetector(config.projectRoot || process.cwd());
|
|
35
|
-
this.formatter = new ResultFormatter();
|
|
36
|
-
this.codeGenerator = new CodeGenerator({
|
|
37
|
-
aiModel: config.aiModel,
|
|
38
|
-
maxRetries: config.maxRetries,
|
|
39
|
-
});
|
|
40
|
-
this.github = new GitHubAPI();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* 主執行流程
|
|
45
|
-
* @param {string|number} issueInput
|
|
46
|
-
* @param {AutodevOptions} options
|
|
47
|
-
*/
|
|
48
|
-
async execute(issueInput, options = {}) {
|
|
49
|
-
const step = (n, total, msg) =>
|
|
50
|
-
console.log(`\n[${n}/${total}] ${msg}`);
|
|
51
|
-
|
|
52
|
-
const TOTAL = this._calcTotalSteps(options);
|
|
53
|
-
let stepIdx = 0;
|
|
54
|
-
|
|
55
|
-
// ── 1. 解析 Issue ────────────────────────────────────────
|
|
56
|
-
step(++stepIdx, TOTAL, '解析 Issue...');
|
|
57
|
-
const issueData = await this._parseIssue(issueInput, options);
|
|
58
|
-
|
|
59
|
-
// ── 2. AI 自動開發(生成代碼框架)────────────────────────
|
|
60
|
-
step(++stepIdx, TOTAL, '生成代碼框架...');
|
|
61
|
-
const generatedFiles = await this._generateCode(issueData, options);
|
|
62
|
-
|
|
63
|
-
// ── 3. 偵測框架 ──────────────────────────────────────────
|
|
64
|
-
step(++stepIdx, TOTAL, '偵測測試框架...');
|
|
65
|
-
const framework = this._detectFramework(options);
|
|
66
|
-
|
|
67
|
-
// ── 4. 發現測試檔案 ──────────────────────────────────────
|
|
68
|
-
step(++stepIdx, TOTAL, '搜尋測試檔案...');
|
|
69
|
-
const testFiles = this.testDetector.discoverTests(framework, {
|
|
70
|
-
testPaths: this.config.testPaths,
|
|
71
|
-
});
|
|
72
|
-
console.log(` 找到 ${testFiles.length} 個測試檔案`);
|
|
73
|
-
|
|
74
|
-
if (options.dryRun) {
|
|
75
|
-
console.log('\n🔍 乾運行模式:以下是執行計劃\n');
|
|
76
|
-
console.log(` Issue : #${issueData.number} - ${issueData.title}`);
|
|
77
|
-
console.log(` 代碼檔案 : ${generatedFiles.length} 個將被建立`);
|
|
78
|
-
console.log(` 框架 : ${framework}`);
|
|
79
|
-
console.log(` 測試數量 : ${testFiles.length} 個檔案`);
|
|
80
|
-
console.log(` Commit : ${this._willCommit(options) ? '✅ 會執行' : '⏭️ 跳過'}`);
|
|
81
|
-
console.log(` PR : ${this._willCreatePR(options) ? '✅ 會執行' : '⏭️ 跳過'}`);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (testFiles.length === 0) {
|
|
86
|
-
console.warn(' ⚠️ 未找到任何測試檔案,跳過測試步驟');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ── 5. 執行測試 ──────────────────────────────────────────
|
|
90
|
-
step(++stepIdx, TOTAL, `執行 ${framework} 測試...`);
|
|
91
|
-
const executor = createExecutor(framework, {
|
|
92
|
-
timeout: this.config.testTimeout,
|
|
93
|
-
});
|
|
94
|
-
const testResults = await executor.run(testFiles, {
|
|
95
|
-
verbose: options.verbose,
|
|
96
|
-
projectRoot: this.config.projectRoot || process.cwd(),
|
|
97
|
-
});
|
|
98
|
-
const allPassed = testResults.failed === 0 && testResults.total > 0;
|
|
99
|
-
const passRate = testResults.total > 0
|
|
100
|
-
? ((testResults.passed / testResults.total) * 100).toFixed(1)
|
|
101
|
-
: '-';
|
|
102
|
-
console.log(
|
|
103
|
-
` ${allPassed ? '✅' : '❌'} ${testResults.passed}/${testResults.total} 通過(${passRate}%)· ${(testResults.duration / 1000).toFixed(1)}s`
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
// ── 6. 格式化結果 ─────────────────────────────────────────
|
|
107
|
-
step(++stepIdx, TOTAL, '格式化測試結果...');
|
|
108
|
-
const meta = {
|
|
109
|
-
generatedFiles: generatedFiles.length,
|
|
110
|
-
};
|
|
111
|
-
let markdown = this.formatter.formatAsMarkdown(testResults, issueData, meta);
|
|
112
|
-
|
|
113
|
-
// ── 7. 發佈到 Issue 評論(初稿,無 commit/PR 資訊)─────────
|
|
114
|
-
step(++stepIdx, TOTAL, `發佈結果到 Issue #${issueData.number}...`);
|
|
115
|
-
const commentResult = await this._postOrUpdateComment(
|
|
116
|
-
issueData,
|
|
117
|
-
markdown,
|
|
118
|
-
options
|
|
119
|
-
);
|
|
120
|
-
if (commentResult) {
|
|
121
|
-
console.log(` 📝 評論已發佈:${commentResult.url}`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ── 8. 若測試失敗,停止 ───────────────────────────────────
|
|
125
|
-
if (!allPassed && testResults.total > 0) {
|
|
126
|
-
console.log('\n❌ 測試未全部通過,已停止自動 commit / PR');
|
|
127
|
-
console.log(' 修復失敗的測試後,重新執行 `ai autodev <issue>`');
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// 若沒有測試:預設停止,加 --skip-tests 才繼續
|
|
132
|
-
if (testResults.total === 0) {
|
|
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
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ── 9. 自動 commit-all ───────────────────────────────────
|
|
155
|
-
if (this._willCommit(options)) {
|
|
156
|
-
step(++stepIdx, TOTAL, '自動提交(ai commit-all)...');
|
|
157
|
-
const commitResult = await commitAllProgrammatic({ verbose: options.verbose });
|
|
158
|
-
if (!commitResult.success) {
|
|
159
|
-
console.error(` ❌ 提交失敗:${commitResult.message}`);
|
|
160
|
-
console.log(' 請手動執行 `ai commit-all`,然後再次執行 `ai pr`');
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
meta.commitHash = commitResult.commitHash;
|
|
164
|
-
console.log(` ✅ ${commitResult.message}(${commitResult.commitHash})`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ── 10. 自動建立 PR ───────────────────────────────────────
|
|
168
|
-
if (this._willCreatePR(options)) {
|
|
169
|
-
step(++stepIdx, TOTAL, '自動建立 PR(ai pr)...');
|
|
170
|
-
const prResult = await prProgrammatic({ verbose: options.verbose });
|
|
171
|
-
if (!prResult.success) {
|
|
172
|
-
console.error(` ❌ PR 建立失敗:${prResult.message}`);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
meta.prUrl = prResult.prUrl;
|
|
176
|
-
console.log(` ✅ PR 已建立:${prResult.prUrl}`);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// ── 更新 Issue 評論(加入 commit/PR 資訊)───────────────
|
|
180
|
-
if (commentResult && (meta.commitHash || meta.prUrl)) {
|
|
181
|
-
markdown = this.formatter.formatAsMarkdown(testResults, issueData, meta);
|
|
182
|
-
await this._postOrUpdateComment(issueData, markdown, options, commentResult.id);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// ── 完成摘要 ──────────────────────────────────────────────
|
|
186
|
-
console.log('\n' + '═'.repeat(60));
|
|
187
|
-
console.log('🎉 ai autodev 完成!');
|
|
188
|
-
console.log(` Issue : #${issueData.number} - ${issueData.title}`);
|
|
189
|
-
if (generatedFiles.length > 0) console.log(` 代碼檔案 : ${generatedFiles.length} 個已建立`);
|
|
190
|
-
console.log(` 測試 : ${testResults.passed}/${testResults.total} 通過`);
|
|
191
|
-
if (meta.commitHash) console.log(` Commit : ${meta.commitHash}`);
|
|
192
|
-
if (meta.prUrl) console.log(` PR : ${meta.prUrl}`);
|
|
193
|
-
if (commentResult) console.log(` 評論 : ${commentResult.url}`);
|
|
194
|
-
console.log('═'.repeat(60));
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// ── 私有輔助方法 ─────────────────────────────────────────────
|
|
198
|
-
|
|
199
|
-
async _parseIssue(issueInput, options) {
|
|
200
|
-
const issueData = await this.issueParser.parse(issueInput);
|
|
201
|
-
if (options.verbose) {
|
|
202
|
-
console.log(` #${issueData.number}: ${issueData.title}`);
|
|
203
|
-
console.log(` 倉庫: ${issueData.owner}/${issueData.repo}`);
|
|
204
|
-
}
|
|
205
|
-
return issueData;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async _generateCode(issueData, options) {
|
|
209
|
-
try {
|
|
210
|
-
const projectRoot = this.config.projectRoot || process.cwd();
|
|
211
|
-
const files = await this.codeGenerator.generateCodeFiles(issueData, projectRoot);
|
|
212
|
-
|
|
213
|
-
if (files.length === 0) {
|
|
214
|
-
console.log(' ℹ️ 沒有需要生成的代碼檔案');
|
|
215
|
-
return [];
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// 寫入檔案(每個檔案會即時顯示名稱)
|
|
219
|
-
const { written, skipped } = await this.codeGenerator.writeFiles(files, projectRoot);
|
|
220
|
-
|
|
221
|
-
if (skipped.length > 0) {
|
|
222
|
-
console.warn(` ⚠️ 跳過 ${skipped.length} 個檔案`);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// 顯示 git diff --stat 讓用戶看到實際變動
|
|
226
|
-
if (written.length > 0) {
|
|
227
|
-
console.log(`\n ✅ 共寫入 ${written.length} 個檔案,git 變動摘要:`);
|
|
228
|
-
try {
|
|
229
|
-
const { execSync } = await import('child_process');
|
|
230
|
-
const stat = execSync('git diff --stat HEAD 2>/dev/null || git status --short', {
|
|
231
|
-
cwd: projectRoot,
|
|
232
|
-
encoding: 'utf-8',
|
|
233
|
-
}).trim();
|
|
234
|
-
if (stat) {
|
|
235
|
-
stat.split('\n').forEach((line) => console.log(` ${line}`));
|
|
236
|
-
}
|
|
237
|
-
} catch (_) {
|
|
238
|
-
// git diff 失敗不影響流程
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return written;
|
|
243
|
-
} catch (error) {
|
|
244
|
-
console.error(` ❌ 代碼生成失敗:${error.message}`);
|
|
245
|
-
return [];
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
_detectFramework(options) {
|
|
250
|
-
const forced = options.framework || this.config.framework;
|
|
251
|
-
if (forced) {
|
|
252
|
-
console.log(` 使用指定框架:${forced}`);
|
|
253
|
-
return forced;
|
|
254
|
-
}
|
|
255
|
-
const detected = this.testDetector.detectFramework();
|
|
256
|
-
if (!detected) {
|
|
257
|
-
throw new Error(
|
|
258
|
-
'無法自動偵測測試框架\n請在 .ai-git-config.js 的 autodev.framework 指定:jest / vitest / mocha'
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
console.log(` 偵測到框架:${detected}`);
|
|
262
|
-
return detected;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async _postOrUpdateComment(issueData, markdown, options, existingCommentId = null) {
|
|
266
|
-
if (options.dryRun) return null;
|
|
267
|
-
|
|
268
|
-
try {
|
|
269
|
-
const { owner, repo, number } = issueData;
|
|
270
|
-
|
|
271
|
-
// 若有已知 ID,直接更新
|
|
272
|
-
if (existingCommentId) {
|
|
273
|
-
return this.github.editIssueComment(owner, repo, existingCommentId, markdown);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// 搜尋相同 marker 的舊評論
|
|
277
|
-
const existing = this.github.findCommentWithMarker(owner, repo, number, COMMENT_MARKER);
|
|
278
|
-
if (existing) {
|
|
279
|
-
return this.github.editIssueComment(owner, repo, existing.id, markdown);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return this.github.postCommentOnIssue(owner, repo, number, markdown);
|
|
283
|
-
} catch (error) {
|
|
284
|
-
console.error(` ⚠️ 發佈評論失敗:${error.message}`);
|
|
285
|
-
return null;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
_willCommit(options) {
|
|
290
|
-
return !options.skipAll && !options.skipCommit && !options.dryRun;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
_willCreatePR(options) {
|
|
294
|
-
if (options.commitOnly) return false;
|
|
295
|
-
return !options.skipAll && !options.skipPr && !options.dryRun;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
_calcTotalSteps(options) {
|
|
299
|
-
// 基本步驟:Issue解析 + 代碼生成 + 框架偵測 + 測試發現 + 執行 + 格式化 + 發佈
|
|
300
|
-
let count = 7;
|
|
301
|
-
if (this._willCommit(options)) count++;
|
|
302
|
-
if (this._willCreatePR(options)) count++;
|
|
303
|
-
return count;
|
|
304
|
-
}
|
|
305
|
-
}
|