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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-git-tools",
3
- "version": "2.0.40",
3
+ "version": "2.0.42",
4
4
  "description": "AI-powered Git automation tools for commit messages and PR generation",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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
- userAutodev = (imported.default || {}).autodev || {};
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
- // ── 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;