ai-git-tools 2.0.40 → 2.0.41

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-git-tools",
3
- "version": "2.0.40",
3
+ "version": "2.0.41",
4
4
  "description": "AI-powered Git automation tools for commit messages and PR generation",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -0,0 +1,169 @@
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
+ 3. 生成初始實現(可以有 TODO 註釋表示待完成部分)
114
+
115
+ ## 輸出格式
116
+ 請回傳 JSON 陣列,每個物件包含:
117
+ \`\`\`json
118
+ [
119
+ {
120
+ "filePath": "src/components/MyComponent.tsx",
121
+ "content": "// 完整的檔案內容...",
122
+ "type": "new"
123
+ },
124
+ {
125
+ "filePath": "src/utils/helper.ts",
126
+ "content": "// ...",
127
+ "type": "new"
128
+ }
129
+ ]
130
+ \`\`\`
131
+
132
+ 只回傳 JSON,不要其他文字。
133
+
134
+ ## 檔案類型指南
135
+ - TypeScript/JavaScript:包含正確的 import/export
136
+ - React:使用 functional components + hooks
137
+ - 包含基本的 JSDoc 註釋
138
+ - 包含 TODO 或 FIXME 註釋提示需要補完的部分
139
+
140
+ 專案根目錄:${projectRoot}`;
141
+ }
142
+
143
+ _parseResponse(response) {
144
+ try {
145
+ // 找 JSON 陣列
146
+ const match = response.match(/\[[\s\S]*\]/);
147
+ if (!match) {
148
+ console.warn(' ⚠️ 無法從 AI 回應找到 JSON 陣列');
149
+ return [];
150
+ }
151
+
152
+ const files = JSON.parse(match[0]);
153
+ if (!Array.isArray(files)) {
154
+ console.warn(' ⚠️ AI 回應不是陣列格式');
155
+ return [];
156
+ }
157
+
158
+ return files.map((f) => ({
159
+ filePath: f.filePath || '',
160
+ content: f.content || '',
161
+ type: f.type || 'new',
162
+ reason: f.reason || null,
163
+ }));
164
+ } catch (error) {
165
+ console.warn(` ⚠️ 解析 AI 回應失敗:${error.message}`);
166
+ return [];
167
+ }
168
+ }
169
+ }
@@ -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
- // ── 3. 發現測試檔案 ──────────────────────────────────────
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 : #${issueData.number} - ${issueData.title}`);
67
- console.log(` 框架 : ${framework}`);
68
- console.log(` 測試數量: ${testFiles.length} 個檔案`);
69
- console.log(` Commit : ${this._willCommit(options) ? '✅ 會執行' : '⏭️ 跳過'}`);
70
- console.log(` PR : ${this._willCreatePR(options) ? '✅ 會執行' : '⏭️ 跳過'}`);
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
- // ── 4. 執行測試 ──────────────────────────────────────────
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
- // ── 5. 格式化結果 ─────────────────────────────────────────
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
- // ── 6. 發佈到 Issue 評論(初稿,無 commit/PR 資訊)─────────
101
- step(++stepIdx, TOTAL, `發佈測試結果到 Issue #${issueData.number}...`);
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
- // ── 7. 若測試失敗,停止 ───────────────────────────────────
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
- // ── 8. 自動 commit-all ───────────────────────────────────
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
- // ── 9. 自動建立 PR ────────────────────────────────────────
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 : #${issueData.number} - ${issueData.title}`);
158
- console.log(` 測試 : ${testResults.passed}/${testResults.total} 通過`);
159
- if (meta.commitHash) console.log(` Commit : ${meta.commitHash}`);
160
- if (meta.prUrl) console.log(` PR : ${meta.prUrl}`);
161
- if (commentResult) console.log(` 評論 : ${commentResult.url}`);
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 = 6;
267
+ // 基本步驟:Issue解析 + 代碼生成 + 框架偵測 + 測試發現 + 執行 + 格式化 + 發佈
268
+ let count = 7;
228
269
  if (this._willCommit(options)) count++;
229
270
  if (this._willCreatePR(options)) count++;
230
271
  return count;