ai-git-tools 2.0.0 → 2.0.1
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 +7 -7
- package/package.json +1 -1
- package/src/commands/commit-all.js +82 -82
- package/src/commands/commit.js +40 -40
- package/src/commands/init.js +22 -22
- package/src/commands/pr.js +23 -23
- package/src/core/ai-client.js +8 -8
- package/src/core/config-loader.js +17 -17
- package/src/core/git-operations.js +21 -21
- package/src/utils/helpers.js +14 -14
- package/src/utils/logger.js +1 -1
package/bin/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* AI Git Tools CLI
|
|
5
5
|
*
|
|
6
6
|
* AI-powered Git automation for commit messages and PR generation
|
|
7
|
-
*
|
|
7
|
+
* 完全重写版本基於 scripts/ 原始实现
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { Command } from 'commander';
|
|
@@ -23,7 +23,7 @@ program
|
|
|
23
23
|
// Init 命令
|
|
24
24
|
program
|
|
25
25
|
.command('init')
|
|
26
|
-
.description('
|
|
26
|
+
.description('初始化配置檔案 (.ai-git-config.mjs)')
|
|
27
27
|
.action(initCommand);
|
|
28
28
|
|
|
29
29
|
// Commit 命令
|
|
@@ -31,7 +31,7 @@ program
|
|
|
31
31
|
.command('commit')
|
|
32
32
|
.description('AI 自动生成 commit message 并提交')
|
|
33
33
|
.option('--model <model>', '指定 AI 模型')
|
|
34
|
-
.option('-v, --verbose', '
|
|
34
|
+
.option('-v, --verbose', '顯示详细输出')
|
|
35
35
|
.option('--max-diff <number>', '最大 diff 长度')
|
|
36
36
|
.option('--max-retries <number>', '最大重试次数')
|
|
37
37
|
.action(commitCommand);
|
|
@@ -39,9 +39,9 @@ program
|
|
|
39
39
|
// Commit All 命令
|
|
40
40
|
program
|
|
41
41
|
.command('commit-all')
|
|
42
|
-
.description('
|
|
42
|
+
.description('智慧分析所有變更并自动分組提交')
|
|
43
43
|
.option('--model <model>', '指定 AI 模型')
|
|
44
|
-
.option('-v, --verbose', '
|
|
44
|
+
.option('-v, --verbose', '顯示详细输出')
|
|
45
45
|
.option('--max-diff <number>', '最大 diff 长度')
|
|
46
46
|
.option('--max-retries <number>', '最大重试次数')
|
|
47
47
|
.action(commitAllCommand);
|
|
@@ -53,10 +53,10 @@ program
|
|
|
53
53
|
.option('--base <branch>', '指定目标分支')
|
|
54
54
|
.option('--head <branch>', '指定来源分支')
|
|
55
55
|
.option('--model <model>', '指定 AI 模型')
|
|
56
|
-
.option('--org <org-name>', '指定 GitHub
|
|
56
|
+
.option('--org <org-name>', '指定 GitHub 組织名称')
|
|
57
57
|
.option('--draft', '创建草稿 PR')
|
|
58
58
|
.option('--preview', '仅预览 PR 内容,不实际创建')
|
|
59
|
-
.option('--no-confirm', '
|
|
59
|
+
.option('--no-confirm', '跳過确认直接创建')
|
|
60
60
|
.option('--auto-reviewers', '自动选择 reviewers')
|
|
61
61
|
.option('--auto-labels', '自动添加 Labels')
|
|
62
62
|
.option('--no-labels', '不添加 Labels')
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Commit All 命令
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* 基於 scripts/ai-auto-commit-all.mjs
|
|
4
|
+
* 智慧分析所有變更並自動分類提交
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { execSync } from 'child_process';
|
|
@@ -12,28 +12,28 @@ import { Logger } from '../utils/logger.js';
|
|
|
12
12
|
import { handleError } from '../utils/helpers.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* 獲取檔案的變更內容
|
|
16
16
|
*/
|
|
17
17
|
function getFileDiff(filePath, isNew) {
|
|
18
18
|
try {
|
|
19
19
|
if (isNew) {
|
|
20
|
-
//
|
|
20
|
+
// 新檔案:讀取完整內容(前 100 行)
|
|
21
21
|
const content = readFileSync(filePath, 'utf-8');
|
|
22
22
|
const lines = content.split('\n').slice(0, 100);
|
|
23
|
-
return `[
|
|
23
|
+
return `[新檔案]\n${lines.join('\n')}${lines.length >= 100 ? '\n...' : ''}`;
|
|
24
24
|
}
|
|
25
|
-
//
|
|
25
|
+
// 已存在檔案:獲取 diff
|
|
26
26
|
const diff = execSync(`git diff HEAD -- "${filePath}"`, {
|
|
27
27
|
encoding: 'utf-8',
|
|
28
28
|
}).toString();
|
|
29
|
-
return diff || '[
|
|
29
|
+
return diff || '[無變更]';
|
|
30
30
|
} catch (error) {
|
|
31
|
-
return `[
|
|
31
|
+
return `[讀取錯誤: ${error.message}]`;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
36
|
+
* 獲取所有未提交的變更
|
|
37
37
|
*/
|
|
38
38
|
function getAllChanges() {
|
|
39
39
|
try {
|
|
@@ -52,12 +52,12 @@ function getAllChanges() {
|
|
|
52
52
|
const statusCode = line.substring(0, 2);
|
|
53
53
|
const filePath = line.substring(3).trim();
|
|
54
54
|
|
|
55
|
-
//
|
|
55
|
+
// 跳過已刪除的檔案
|
|
56
56
|
if (statusCode.includes('D')) {
|
|
57
57
|
continue;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
//
|
|
60
|
+
// 跳過某些不需要提交的檔案
|
|
61
61
|
if (
|
|
62
62
|
filePath.includes('node_modules/') ||
|
|
63
63
|
filePath.includes('.next/') ||
|
|
@@ -80,115 +80,115 @@ function getAllChanges() {
|
|
|
80
80
|
|
|
81
81
|
return changes;
|
|
82
82
|
} catch (error) {
|
|
83
|
-
console.error('
|
|
83
|
+
console.error('獲取變更列表失敗:', error.message);
|
|
84
84
|
return [];
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
|
-
* 使用 AI
|
|
89
|
+
* 使用 AI 分析並分組變更
|
|
90
90
|
*/
|
|
91
91
|
async function analyzeAndGroupChanges(changes, config) {
|
|
92
|
-
console.log('🤖 正在使用 AI
|
|
92
|
+
console.log('🤖 正在使用 AI 分析變更並分組...\n');
|
|
93
93
|
|
|
94
|
-
//
|
|
94
|
+
// 準備變更摘要
|
|
95
95
|
const maxDiffPerFile = Math.floor(config.ai.maxDiffLength / Math.max(changes.length, 1));
|
|
96
96
|
const changeSummary = changes
|
|
97
97
|
.map((change, index) => {
|
|
98
98
|
const diff = getFileDiff(change.filePath, change.isNew);
|
|
99
99
|
const lines = diff.split('\n');
|
|
100
100
|
const truncatedDiff = lines.slice(0, Math.min(50, maxDiffPerFile / 100)).join('\n');
|
|
101
|
-
return `[
|
|
102
|
-
change.isNew ? '
|
|
101
|
+
return `[檔案 ${index}] ${change.filePath}\n${
|
|
102
|
+
change.isNew ? '(新檔案)' : '(已修改)'
|
|
103
103
|
}\n${truncatedDiff}\n`;
|
|
104
104
|
})
|
|
105
105
|
.join('\n---\n\n');
|
|
106
106
|
|
|
107
|
-
const prompt =
|
|
107
|
+
const prompt = `你是一個資深前端工程師,熟悉 Next.js 專案的開發規範。請分析以下的檔案變更,並將它們按照功能/目的分組。
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
**專案背景**:
|
|
110
110
|
- Next.js 12+ (Pages Router)
|
|
111
111
|
- TypeScript + JavaScript 混合
|
|
112
112
|
- Tailwind CSS + Styled Components
|
|
113
|
-
- Zustand (
|
|
114
|
-
- React Hook Form + Zod (
|
|
113
|
+
- Zustand (客戶端狀態) + SWR (伺服器資料獲取)
|
|
114
|
+
- React Hook Form + Zod (表單處理)
|
|
115
115
|
- 架构:Modified Atomic Design(UI / Page / Feature 三层)
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
- pages/ →
|
|
119
|
-
- components/Page/ →
|
|
117
|
+
**專案目錄結構參考**:
|
|
118
|
+
- pages/ → 頁面路由
|
|
119
|
+
- components/Page/ → 頁面級元件
|
|
120
120
|
- components/UI/ 或 components/Common/ → 共用 UI 元件
|
|
121
|
-
- components/[Feature]/ →
|
|
122
|
-
- store/ → Zustand
|
|
121
|
+
- components/[Feature]/ → 功能模組元件
|
|
122
|
+
- store/ → Zustand 狀態管理
|
|
123
123
|
- api/ → API 呼叫
|
|
124
124
|
- utils/ → 工具函式
|
|
125
|
-
- styles/ →
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
1.
|
|
129
|
-
2.
|
|
130
|
-
3.
|
|
131
|
-
4.
|
|
132
|
-
5.
|
|
133
|
-
- group_name:
|
|
134
|
-
- commit_type: commit
|
|
135
|
-
- commit_scope: commit
|
|
136
|
-
- file_indices:
|
|
137
|
-
- description:
|
|
138
|
-
|
|
139
|
-
|
|
125
|
+
- styles/ → 全域樣式
|
|
126
|
+
|
|
127
|
+
規則:
|
|
128
|
+
1. 將相關功能的變更歸類在同一組(例如:同一個功能開發、同一個 bug 修復、相關的重構等)
|
|
129
|
+
2. 每組應該要有明確的主題
|
|
130
|
+
3. 同一個功能的元件、API、store、樣式應歸為同一組
|
|
131
|
+
4. 設定檔(config)和檔案(docs)變更可以獨立成一組
|
|
132
|
+
5. 輸出格式為 JSON 陣列,每個元素包含:
|
|
133
|
+
- group_name: 群組名稱(簡短描述,繁體中文)
|
|
134
|
+
- commit_type: commit 類型(feat/fix/docs/style/refactor/test/chore/perf)
|
|
135
|
+
- commit_scope: commit 影響範圍(如 member、report、auth、api、ui、config)
|
|
136
|
+
- file_indices: 屬於這組的檔案索引陣列(對應上面的 [檔案 X])
|
|
137
|
+
- description: 這組變更的詳細說明(繁體中文)
|
|
138
|
+
|
|
139
|
+
範例輸出:
|
|
140
140
|
[
|
|
141
141
|
{
|
|
142
142
|
"group_name": "新增使用者登入功能",
|
|
143
143
|
"commit_type": "feat",
|
|
144
144
|
"commit_scope": "auth",
|
|
145
145
|
"file_indices": [0, 1, 2],
|
|
146
|
-
"description": "
|
|
146
|
+
"description": "實作使用者登入 API 和前端頁面"
|
|
147
147
|
},
|
|
148
148
|
{
|
|
149
|
-
"group_name": "
|
|
149
|
+
"group_name": "修正導航列手機版顯示",
|
|
150
150
|
"commit_type": "fix",
|
|
151
151
|
"commit_scope": "ui",
|
|
152
152
|
"file_indices": [3, 4],
|
|
153
|
-
"description": "
|
|
153
|
+
"description": "修正導航列在手機版的顯示問題"
|
|
154
154
|
}
|
|
155
155
|
]
|
|
156
156
|
|
|
157
|
-
|
|
157
|
+
檔案變更內容:
|
|
158
158
|
${changeSummary}
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
請只輸出 JSON,不要其他文字。`;
|
|
161
161
|
|
|
162
162
|
const response = await AIClient.sendAndWait(prompt, config.ai.model);
|
|
163
163
|
|
|
164
164
|
try {
|
|
165
165
|
return AIClient.parseJSON(response);
|
|
166
166
|
} catch (error) {
|
|
167
|
-
console.error('❌
|
|
168
|
-
console.log('
|
|
167
|
+
console.error('❌ 無法解析 AI 回應:', error.message);
|
|
168
|
+
console.log('原始回應:', response);
|
|
169
169
|
return null;
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
/**
|
|
174
|
-
*
|
|
174
|
+
* 为特定群組生成 commit message
|
|
175
175
|
*/
|
|
176
176
|
async function generateCommitMessage(group, files, config) {
|
|
177
177
|
const filesList = files
|
|
178
178
|
.map((file) => {
|
|
179
179
|
const diff = getFileDiff(file.filePath, file.isNew);
|
|
180
|
-
return
|
|
180
|
+
return `檔案: ${file.filePath}\n${diff}`;
|
|
181
181
|
})
|
|
182
182
|
.join('\n\n---\n\n');
|
|
183
183
|
|
|
184
184
|
const prompt = `请根据以下资讯生成一则 commit message:
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
群組名称: ${group.group_name}
|
|
187
187
|
Commit 类型: ${group.commit_type}
|
|
188
188
|
Commit 范围: ${group.commit_scope || '未指定'}
|
|
189
189
|
说明: ${group.description}
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
檔案變更:
|
|
192
192
|
${filesList}
|
|
193
193
|
|
|
194
194
|
规则:
|
|
@@ -196,7 +196,7 @@ ${filesList}
|
|
|
196
196
|
group.commit_scope ? `(${group.commit_scope})` : ''
|
|
197
197
|
}: <subject>
|
|
198
198
|
- subject 限制在 50 字内,使用繁体中文
|
|
199
|
-
-
|
|
199
|
+
- 如果變更複雜,可以加上 body(用空行分隔),body 使用 bullet points
|
|
200
200
|
- 只输出 commit message 本身,不要其他说明
|
|
201
201
|
- 不要包含 markdown code block 标记(不要 \`\`\`)
|
|
202
202
|
- 不要加上任何引导语句
|
|
@@ -221,30 +221,30 @@ feat(auth): 新增使用者登入功能
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
/**
|
|
224
|
-
*
|
|
224
|
+
* 執行分組提交
|
|
225
225
|
*/
|
|
226
226
|
async function commitGroup(group, files, config) {
|
|
227
227
|
try {
|
|
228
|
-
console.log(`\n📦
|
|
228
|
+
console.log(`\n📦 处理群組: ${group.group_name}`);
|
|
229
229
|
console.log(
|
|
230
230
|
` 类型: ${group.commit_type}${group.commit_scope ? `(${group.commit_scope})` : ''}`
|
|
231
231
|
);
|
|
232
|
-
console.log(`
|
|
232
|
+
console.log(` 檔案数量: ${files.length}`);
|
|
233
233
|
|
|
234
|
-
// 先 reset 所有已 staged
|
|
234
|
+
// 先 reset 所有已 staged 的檔案
|
|
235
235
|
try {
|
|
236
236
|
execSync('git reset HEAD -- .', { stdio: 'ignore' });
|
|
237
237
|
} catch (e) {
|
|
238
|
-
//
|
|
238
|
+
// 忽略錯誤(可能没有 staged 的檔案)
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
// Add
|
|
241
|
+
// Add 这組的檔案
|
|
242
242
|
for (const file of files) {
|
|
243
243
|
console.log(` ├─ ${file.filePath}`);
|
|
244
244
|
try {
|
|
245
245
|
execSync(`git add "${file.filePath}"`, { encoding: 'utf-8' });
|
|
246
246
|
} catch (addError) {
|
|
247
|
-
console.error(` ⚠️
|
|
247
|
+
console.error(` ⚠️ 無法加入檔案: ${file.filePath}`, addError.message);
|
|
248
248
|
throw addError;
|
|
249
249
|
}
|
|
250
250
|
}
|
|
@@ -254,7 +254,7 @@ async function commitGroup(group, files, config) {
|
|
|
254
254
|
const commitMessage = await generateCommitMessage(group, files, config);
|
|
255
255
|
|
|
256
256
|
if (!commitMessage) {
|
|
257
|
-
console.log(` ❌
|
|
257
|
+
console.log(` ❌ 無法生成 commit message,跳過此群組`);
|
|
258
258
|
return false;
|
|
259
259
|
}
|
|
260
260
|
|
|
@@ -265,8 +265,8 @@ async function commitGroup(group, files, config) {
|
|
|
265
265
|
});
|
|
266
266
|
console.log(` ${'─'.repeat(50)}`);
|
|
267
267
|
|
|
268
|
-
//
|
|
269
|
-
//
|
|
268
|
+
// 執行 commit
|
|
269
|
+
// 使用暫存檔案避免 commit message 中的特殊字元問題
|
|
270
270
|
const tmpFile = '.git/COMMIT_EDITMSG_TMP';
|
|
271
271
|
try {
|
|
272
272
|
writeFileSync(tmpFile, commitMessage, 'utf-8');
|
|
@@ -278,7 +278,7 @@ async function commitGroup(group, files, config) {
|
|
|
278
278
|
try {
|
|
279
279
|
unlinkSync(tmpFile);
|
|
280
280
|
} catch (e) {
|
|
281
|
-
//
|
|
281
|
+
// 忽略刪除临时檔案的錯誤
|
|
282
282
|
}
|
|
283
283
|
throw commitError;
|
|
284
284
|
}
|
|
@@ -286,7 +286,7 @@ async function commitGroup(group, files, config) {
|
|
|
286
286
|
console.log(` ✅ Commit 完成!`);
|
|
287
287
|
return true;
|
|
288
288
|
} catch (error) {
|
|
289
|
-
console.error(` ❌ Commit
|
|
289
|
+
console.error(` ❌ Commit 失敗:`, error.message);
|
|
290
290
|
return false;
|
|
291
291
|
}
|
|
292
292
|
}
|
|
@@ -298,10 +298,10 @@ export async function commitAllCommand() {
|
|
|
298
298
|
const logger = new Logger();
|
|
299
299
|
|
|
300
300
|
try {
|
|
301
|
-
//
|
|
301
|
+
// 載入配置
|
|
302
302
|
const config = await loadCommitConfig();
|
|
303
303
|
|
|
304
|
-
logger.header('
|
|
304
|
+
logger.header('智慧分析所有變更並自動提交');
|
|
305
305
|
|
|
306
306
|
if (config.output.verbose) {
|
|
307
307
|
console.log('📋 使用配置:');
|
|
@@ -311,39 +311,39 @@ export async function commitAllCommand() {
|
|
|
311
311
|
console.log('');
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
// 1.
|
|
315
|
-
logger.step('
|
|
314
|
+
// 1. 獲取所有變更
|
|
315
|
+
logger.step('掃描變更中...');
|
|
316
316
|
const changes = getAllChanges();
|
|
317
317
|
|
|
318
318
|
if (changes.length === 0) {
|
|
319
|
-
logger.info('
|
|
319
|
+
logger.info('沒有需要提交的變更');
|
|
320
320
|
process.exit(0);
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
-
console.log(`📊 找到 ${changes.length}
|
|
323
|
+
console.log(`📊 找到 ${changes.length} 个變更的檔案:\n`);
|
|
324
324
|
changes.forEach((change, index) => {
|
|
325
325
|
const status = change.isNew ? '新增' : '修改';
|
|
326
326
|
console.log(` [${index}] ${status} - ${change.filePath}`);
|
|
327
327
|
});
|
|
328
328
|
console.log();
|
|
329
329
|
|
|
330
|
-
// 2. 使用 AI
|
|
330
|
+
// 2. 使用 AI 分析並分組
|
|
331
331
|
const groups = await analyzeAndGroupChanges(changes, config);
|
|
332
332
|
|
|
333
333
|
if (!groups || groups.length === 0) {
|
|
334
|
-
logger.error('AI
|
|
334
|
+
logger.error('AI 分析失敗或沒有產生分組');
|
|
335
335
|
process.exit(1);
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
logger.success(`AI
|
|
338
|
+
logger.success(`AI 分析完成,共分為 ${groups.length} 個群組:\n`);
|
|
339
339
|
groups.forEach((group, index) => {
|
|
340
|
-
console.log(`
|
|
341
|
-
console.log(` └─ 包含 ${group.file_indices.length}
|
|
340
|
+
console.log(` 群組 ${index + 1}: ${group.group_name} (${group.commit_type})`);
|
|
341
|
+
console.log(` └─ 包含 ${group.file_indices.length} 个檔案`);
|
|
342
342
|
});
|
|
343
343
|
|
|
344
|
-
// 3.
|
|
344
|
+
// 3. 依序提交每个群組
|
|
345
345
|
logger.separator('=', 60);
|
|
346
|
-
console.log('
|
|
346
|
+
console.log('開始執行提交...');
|
|
347
347
|
logger.separator('=', 60);
|
|
348
348
|
|
|
349
349
|
let successCount = 0;
|
|
@@ -357,16 +357,16 @@ export async function commitAllCommand() {
|
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
// 4.
|
|
360
|
+
// 4. 顯示摘要
|
|
361
361
|
logger.separator('=', 60);
|
|
362
|
-
logger.success(`完成!成功提交 ${successCount}/${groups.length}
|
|
362
|
+
logger.success(`完成!成功提交 ${successCount}/${groups.length} 个群組`);
|
|
363
363
|
logger.separator('=', 60);
|
|
364
364
|
|
|
365
|
-
//
|
|
365
|
+
// 顯示最近的几个 commits
|
|
366
366
|
console.log('\n📋 最近的 commits:');
|
|
367
367
|
execSync(`git log -${successCount} --oneline`, { stdio: 'inherit' });
|
|
368
368
|
|
|
369
|
-
// Reset
|
|
369
|
+
// Reset 任何剩餘的 staged 檔案
|
|
370
370
|
try {
|
|
371
371
|
execSync('git reset HEAD -- .', { stdio: 'ignore' });
|
|
372
372
|
} catch (e) {
|
package/src/commands/commit.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Commit 命令
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* 基於 scripts/ai-auto-commit.mjs
|
|
4
|
+
* 自動產生 commit message 並執行 commit
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { execSync } from 'child_process';
|
|
@@ -15,7 +15,7 @@ export async function commitCommand() {
|
|
|
15
15
|
const logger = new Logger();
|
|
16
16
|
|
|
17
17
|
try {
|
|
18
|
-
//
|
|
18
|
+
// 載入配置
|
|
19
19
|
const config = await loadCommitConfig();
|
|
20
20
|
|
|
21
21
|
if (config.output.verbose) {
|
|
@@ -25,59 +25,59 @@ export async function commitCommand() {
|
|
|
25
25
|
console.log('');
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
//
|
|
28
|
+
// 檢查是否有 staged 變更
|
|
29
29
|
const diff = GitOperations.getStagedDiff();
|
|
30
30
|
|
|
31
31
|
if (!diff.trim()) {
|
|
32
|
-
logger.error('
|
|
33
|
-
console.log('💡
|
|
32
|
+
logger.error('沒有 staged 的變更');
|
|
33
|
+
console.log('💡 請先使用 git add 來 stage 你的變更');
|
|
34
34
|
process.exit(1);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
logger.step('
|
|
37
|
+
logger.step('正在分析變更內容...\n');
|
|
38
38
|
|
|
39
|
-
//
|
|
39
|
+
// 截斷過長的 diff
|
|
40
40
|
const truncatedDiff =
|
|
41
41
|
diff.length > config.ai.maxDiffLength
|
|
42
|
-
? diff.substring(0, config.ai.maxDiffLength) + '\n\n... [diff
|
|
42
|
+
? diff.substring(0, config.ai.maxDiffLength) + '\n\n... [diff 過長已截斷]'
|
|
43
43
|
: diff;
|
|
44
44
|
|
|
45
45
|
if (config.output.verbose && diff.length > config.ai.maxDiffLength) {
|
|
46
|
-
logger.warning(`Diff
|
|
46
|
+
logger.warning(`Diff 已從 ${diff.length} 字元截斷至 ${config.ai.maxDiffLength} 字元\n`);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// 使用 AI
|
|
49
|
+
// 使用 AI 產生 commit message
|
|
50
50
|
let commitMessage = '';
|
|
51
51
|
let lastError = null;
|
|
52
52
|
|
|
53
53
|
for (let attempt = 1; attempt <= config.ai.maxRetries; attempt++) {
|
|
54
54
|
try {
|
|
55
55
|
if (config.output.verbose && attempt > 1) {
|
|
56
|
-
console.log(`🔄
|
|
56
|
+
console.log(`🔄 重試第 ${attempt}/${config.ai.maxRetries} 次...\n`);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
const prompt = `${getProjectTypePrompt()}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
請根據以下 git diff 產生一則 commit message。
|
|
62
62
|
|
|
63
|
-
**Commit Message
|
|
63
|
+
**Commit Message 規則**:
|
|
64
64
|
1. 使用 Conventional Commits 格式:type(scope): subject
|
|
65
|
-
2. type
|
|
66
|
-
3. scope:
|
|
67
|
-
4. subject 限制在 50
|
|
68
|
-
5.
|
|
65
|
+
2. type 必須是:feat/fix/docs/style/refactor/test/chore/perf 其中之一
|
|
66
|
+
3. scope: 影響範圍(如 member、report、auth、api、ui、config)
|
|
67
|
+
4. subject 限制在 50 字內,使用繁體中文
|
|
68
|
+
5. 如果變更複雜,加上 body 說明(使用 bullet points)
|
|
69
69
|
|
|
70
70
|
**重要**:
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
71
|
+
- 直接輸出 commit message 純文字,不要使用 markdown 程式碼區塊(\`\`\`)
|
|
72
|
+
- 不要加上任何前綴說明或後綴文字
|
|
73
|
+
- 第一行是標題,如有需要可加上空行後的詳細說明
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
feat(member):
|
|
75
|
+
**範例格式**:
|
|
76
|
+
feat(member): 新增會員管理頁面
|
|
77
77
|
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
- 整合 Zustand
|
|
78
|
+
- 實作會員列表查詢功能
|
|
79
|
+
- 新增會員資料編輯表單
|
|
80
|
+
- 整合 Zustand 狀態管理
|
|
81
81
|
|
|
82
82
|
git diff:
|
|
83
83
|
${truncatedDiff}`;
|
|
@@ -85,18 +85,18 @@ ${truncatedDiff}`;
|
|
|
85
85
|
const response = await AIClient.sendAndWait(prompt, config.ai.model);
|
|
86
86
|
commitMessage = cleanCommitMessage(response);
|
|
87
87
|
|
|
88
|
-
//
|
|
88
|
+
// 驗證 commit message
|
|
89
89
|
const validation = validateCommitMessage(commitMessage);
|
|
90
90
|
if (!validation.valid) {
|
|
91
|
-
throw new Error(
|
|
91
|
+
throw new Error(`無效的 commit message: ${validation.reason}`);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
//
|
|
94
|
+
// 成功產生,跳出重試迴圈
|
|
95
95
|
break;
|
|
96
96
|
} catch (error) {
|
|
97
97
|
lastError = error;
|
|
98
98
|
if (config.output.verbose) {
|
|
99
|
-
logger.warning(
|
|
99
|
+
logger.warning(`嘗試 ${attempt} 失敗: ${error.message}\n`);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
if (attempt < config.ai.maxRetries) {
|
|
@@ -105,26 +105,26 @@ ${truncatedDiff}`;
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
//
|
|
108
|
+
// 所有重試都失敗
|
|
109
109
|
if (!commitMessage) {
|
|
110
|
-
logger.error('
|
|
110
|
+
logger.error('無法產生有效的 commit message');
|
|
111
111
|
if (lastError && config.output.verbose) {
|
|
112
|
-
console.log(`
|
|
112
|
+
console.log(` 最後錯誤: ${lastError.message}`);
|
|
113
113
|
}
|
|
114
|
-
console.log('\n💡
|
|
115
|
-
console.log(' 1.
|
|
116
|
-
console.log(' 2.
|
|
117
|
-
console.log(' 3.
|
|
114
|
+
console.log('\n💡 建議:');
|
|
115
|
+
console.log(' 1. 檢查網路連線');
|
|
116
|
+
console.log(' 2. 嘗試更換 AI 模型(使用 --model 參數)');
|
|
117
|
+
console.log(' 3. 確認變更內容不會太複雜或太大');
|
|
118
118
|
process.exit(1);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
logger.success('
|
|
121
|
+
logger.success('產生的 Commit Message:');
|
|
122
122
|
logger.separator('─', 60);
|
|
123
123
|
console.log(commitMessage);
|
|
124
124
|
logger.separator('─', 60);
|
|
125
125
|
|
|
126
|
-
//
|
|
127
|
-
logger.step('\n
|
|
126
|
+
// 執行 commit
|
|
127
|
+
logger.step('\n正在執行 commit...');
|
|
128
128
|
execSync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, {
|
|
129
129
|
stdio: 'inherit',
|
|
130
130
|
});
|
package/src/commands/init.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Init 命令
|
|
3
|
-
*
|
|
3
|
+
* 初始化配置檔案
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { writeFileSync, existsSync } from 'fs';
|
|
@@ -8,38 +8,38 @@ import { resolve } from 'path';
|
|
|
8
8
|
import { Logger } from '../utils/logger.js';
|
|
9
9
|
|
|
10
10
|
const DEFAULT_CONFIG = `/**
|
|
11
|
-
* AI Git Tools
|
|
11
|
+
* AI Git Tools 配置檔案
|
|
12
12
|
*
|
|
13
|
-
*
|
|
13
|
+
* 此檔案用於設定 AI 自動化 Git 工具的行為
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
export default {
|
|
17
|
-
// AI
|
|
17
|
+
// AI 相關配置
|
|
18
18
|
ai: {
|
|
19
19
|
model: 'gpt-4.1', // AI 模型
|
|
20
|
-
maxDiffLength: 8000, // 最大 diff
|
|
21
|
-
maxRetries: 3, //
|
|
20
|
+
maxDiffLength: 8000, // 最大 diff 長度
|
|
21
|
+
maxRetries: 3, // 最大重試次數
|
|
22
22
|
},
|
|
23
23
|
|
|
24
|
-
// GitHub
|
|
24
|
+
// GitHub 相關配置
|
|
25
25
|
github: {
|
|
26
|
-
orgName: 'your-org-name', // GitHub
|
|
27
|
-
defaultBase: 'auto', //
|
|
28
|
-
autoLabels: true, //
|
|
26
|
+
orgName: 'your-org-name', // GitHub 組織名稱
|
|
27
|
+
defaultBase: 'auto', // 預設 base 分支('auto' 為自動偵測)
|
|
28
|
+
autoLabels: true, // 自動新增 Labels
|
|
29
29
|
},
|
|
30
30
|
|
|
31
|
-
// Reviewers
|
|
31
|
+
// Reviewers 相關配置
|
|
32
32
|
reviewers: {
|
|
33
|
-
autoSelect: false, //
|
|
34
|
-
maxSuggested: 5, //
|
|
35
|
-
gitHistoryDepth: 20, // Git
|
|
33
|
+
autoSelect: false, // 自動選擇 reviewers
|
|
34
|
+
maxSuggested: 5, // 最大建議 reviewers 數量
|
|
35
|
+
gitHistoryDepth: 20, // Git 歷史深度
|
|
36
36
|
excludeAuthors: [], // 排除的作者列表
|
|
37
37
|
},
|
|
38
38
|
|
|
39
|
-
//
|
|
39
|
+
// 輸出相關配置
|
|
40
40
|
output: {
|
|
41
|
-
verbose: false, //
|
|
42
|
-
saveHistory: false, //
|
|
41
|
+
verbose: false, // 詳細輸出
|
|
42
|
+
saveHistory: false, // 儲存歷史記錄
|
|
43
43
|
},
|
|
44
44
|
};
|
|
45
45
|
`;
|
|
@@ -49,17 +49,17 @@ export async function initCommand() {
|
|
|
49
49
|
const configPath = resolve(process.cwd(), '.ai-git-config.mjs');
|
|
50
50
|
|
|
51
51
|
if (existsSync(configPath)) {
|
|
52
|
-
logger.warning('
|
|
53
|
-
console.log('
|
|
52
|
+
logger.warning('配置檔案已存在: .ai-git-config.mjs');
|
|
53
|
+
console.log('如要重新初始化,請先刪除現有配置檔案');
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
try {
|
|
58
58
|
writeFileSync(configPath, DEFAULT_CONFIG, 'utf-8');
|
|
59
|
-
logger.success('
|
|
60
|
-
console.log('\n
|
|
59
|
+
logger.success('已建立配置檔案: .ai-git-config.mjs');
|
|
60
|
+
console.log('\n請編輯此檔案以自訂配置');
|
|
61
61
|
} catch (error) {
|
|
62
|
-
logger.error(
|
|
62
|
+
logger.error(`建立配置檔案失敗: ${error.message}`);
|
|
63
63
|
process.exit(1);
|
|
64
64
|
}
|
|
65
65
|
}
|
package/src/commands/pr.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PR 命令
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* 基於 scripts/ai-auto-pr.mjs 簡化版
|
|
4
|
+
* 產生 PR 標題、描述並建立 Pull Request
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { execSync } from 'child_process';
|
|
@@ -16,13 +16,13 @@ import { handleError, getProjectTypePrompt } from '../utils/helpers.js';
|
|
|
16
16
|
*/
|
|
17
17
|
async function generatePRContent(baseBranch, headBranch, config) {
|
|
18
18
|
const logger = new Logger();
|
|
19
|
-
logger.step('AI
|
|
19
|
+
logger.step('AI 正在分析變更并生成 PR 内容...');
|
|
20
20
|
|
|
21
|
-
//
|
|
21
|
+
// 獲取 diff 和 commits
|
|
22
22
|
const diff = GitOperations.getDiff(baseBranch, headBranch);
|
|
23
23
|
const commits = GitOperations.getCommits(baseBranch, headBranch);
|
|
24
24
|
|
|
25
|
-
//
|
|
25
|
+
// 智慧截断 diff
|
|
26
26
|
const truncatedDiff = GitOperations.truncateDiff(diff, config.ai.maxDiffLength);
|
|
27
27
|
|
|
28
28
|
const prompt = `${getProjectTypePrompt()}
|
|
@@ -42,10 +42,10 @@ ${truncatedDiff}
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
**PR 描述应包含**:
|
|
45
|
-
1. ## 📝
|
|
45
|
+
1. ## 📝 變更摘要(简述主要變更)
|
|
46
46
|
2. ## ✨ 主要功能(列出新增功能或修正项目)
|
|
47
|
-
3. ## 🔧
|
|
48
|
-
4. ## ✅
|
|
47
|
+
3. ## 🔧 技术细节(选填,如有重要的技术變更)
|
|
48
|
+
4. ## ✅ 测试(如何测试这些變更)
|
|
49
49
|
|
|
50
50
|
请只输出 JSON,不要其他文字。`;
|
|
51
51
|
|
|
@@ -56,12 +56,12 @@ ${truncatedDiff}
|
|
|
56
56
|
logger.success('PR 内容生成完成\n');
|
|
57
57
|
return prContent;
|
|
58
58
|
} catch (error) {
|
|
59
|
-
throw new Error(
|
|
59
|
+
throw new Error(`無法解析 AI 回应: ${error.message}`);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
64
|
+
* 顯示 PR 预览
|
|
65
65
|
*/
|
|
66
66
|
function displayPreview(prContent, stats) {
|
|
67
67
|
console.log('\n' + '═'.repeat(60));
|
|
@@ -69,7 +69,7 @@ function displayPreview(prContent, stats) {
|
|
|
69
69
|
console.log('═'.repeat(60));
|
|
70
70
|
console.log(`\n标题: ${prContent.title}\n`);
|
|
71
71
|
console.log(`统计: ${stats.stats}`);
|
|
72
|
-
console.log(
|
|
72
|
+
console.log(`檔案数: ${stats.filesChanged} 个檔案\n`);
|
|
73
73
|
console.log('─'.repeat(60));
|
|
74
74
|
console.log('描述:\n');
|
|
75
75
|
console.log(prContent.description);
|
|
@@ -85,10 +85,10 @@ export async function prCommand() {
|
|
|
85
85
|
try {
|
|
86
86
|
logger.header('AI Auto PR Generator');
|
|
87
87
|
|
|
88
|
-
//
|
|
88
|
+
// 載入配置
|
|
89
89
|
const config = await loadPRConfig();
|
|
90
90
|
|
|
91
|
-
//
|
|
91
|
+
// 獲取当前分支
|
|
92
92
|
const currentBranch = GitOperations.getCurrentBranch();
|
|
93
93
|
let headBranch = config.headBranch || currentBranch;
|
|
94
94
|
let baseBranch = config.baseBranch;
|
|
@@ -108,7 +108,7 @@ export async function prCommand() {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
//
|
|
111
|
+
// 檢查是否在 base branch
|
|
112
112
|
if (currentBranch === baseBranch) {
|
|
113
113
|
logger.error(`你目前在 ${baseBranch} 分支,请切换到 feature 分支`);
|
|
114
114
|
console.log('💡 切换到 feature 分支: git checkout <feature-branch>');
|
|
@@ -119,7 +119,7 @@ export async function prCommand() {
|
|
|
119
119
|
console.log(` Base: ${baseBranch}`);
|
|
120
120
|
console.log(` Head: ${headBranch}\n`);
|
|
121
121
|
|
|
122
|
-
//
|
|
122
|
+
// 推送到远端(预览模式跳過)
|
|
123
123
|
if (!config.preview) {
|
|
124
124
|
logger.step(`推送到远端分支: origin/${headBranch}`);
|
|
125
125
|
GitOperations.push(headBranch);
|
|
@@ -132,18 +132,18 @@ export async function prCommand() {
|
|
|
132
132
|
logger.success('同步完成\n');
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
//
|
|
135
|
+
// 獲取變更统计
|
|
136
136
|
const stats = GitOperations.getChangeStats(baseBranch, headBranch);
|
|
137
|
-
console.log(`📈
|
|
138
|
-
console.log(`📁
|
|
137
|
+
console.log(`📈 變更统计: ${stats.stats}`);
|
|
138
|
+
console.log(`📁 影响檔案: ${stats.filesChanged} 个\n`);
|
|
139
139
|
|
|
140
140
|
// 生成 PR 内容
|
|
141
141
|
const prContent = await generatePRContent(baseBranch, headBranch, config);
|
|
142
142
|
|
|
143
|
-
//
|
|
143
|
+
// 顯示预览
|
|
144
144
|
displayPreview(prContent, stats);
|
|
145
145
|
|
|
146
|
-
//
|
|
146
|
+
// 预览模式:仅顯示不创建
|
|
147
147
|
if (config.preview) {
|
|
148
148
|
logger.info('预览模式:未创建 PR');
|
|
149
149
|
return;
|
|
@@ -152,13 +152,13 @@ export async function prCommand() {
|
|
|
152
152
|
// 创建 PR(使用 GitHub CLI)
|
|
153
153
|
logger.step('创建 Pull Request...');
|
|
154
154
|
|
|
155
|
-
//
|
|
155
|
+
// 準備 PR body
|
|
156
156
|
const prBody = prContent.description;
|
|
157
157
|
|
|
158
158
|
// 构建 gh pr create 命令
|
|
159
159
|
let ghCommand = `gh pr create --base ${baseBranch} --head ${headBranch} --title "${prContent.title.replace(/"/g, '\\"')}"`;
|
|
160
160
|
|
|
161
|
-
// 将 body
|
|
161
|
+
// 将 body 写入临时檔案
|
|
162
162
|
const { writeFileSync, unlinkSync } = await import('fs');
|
|
163
163
|
const tmpFile = '/tmp/pr-body-temp.md';
|
|
164
164
|
writeFileSync(tmpFile, prBody, 'utf-8');
|
|
@@ -180,7 +180,7 @@ export async function prCommand() {
|
|
|
180
180
|
} catch (e) {
|
|
181
181
|
// 忽略
|
|
182
182
|
}
|
|
183
|
-
throw new Error(`创建 PR
|
|
183
|
+
throw new Error(`创建 PR 失敗: ${error.message}`);
|
|
184
184
|
}
|
|
185
185
|
} catch (error) {
|
|
186
186
|
handleError(error);
|
package/src/core/ai-client.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AI
|
|
3
|
-
*
|
|
2
|
+
* AI 客戶端
|
|
3
|
+
* 基於 @github/copilot-sdk
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { CopilotClient } from '@github/copilot-sdk';
|
|
7
7
|
|
|
8
8
|
export class AIClient {
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* 發送 prompt 並等待回應(帶重試機制)
|
|
11
11
|
*/
|
|
12
12
|
static async sendAndWait(prompt, model = 'gpt-4.1', maxRetries = 3) {
|
|
13
13
|
const client = new CopilotClient();
|
|
@@ -24,27 +24,27 @@ export class AIClient {
|
|
|
24
24
|
} catch (error) {
|
|
25
25
|
lastError = error;
|
|
26
26
|
if (attempt < maxRetries) {
|
|
27
|
-
console.log(`⚠️ AI
|
|
27
|
+
console.log(`⚠️ AI 請求失敗,重試第 ${attempt}/${maxRetries} 次...`);
|
|
28
28
|
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
|
|
29
29
|
continue;
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
throw new Error(`AI
|
|
34
|
+
throw new Error(`AI 請求失敗: ${lastError?.message || '未知錯誤'}`);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* 解析 JSON
|
|
38
|
+
* 解析 JSON 回應
|
|
39
39
|
*/
|
|
40
40
|
static parseJSON(content) {
|
|
41
|
-
// 移除可能的 markdown code block
|
|
41
|
+
// 移除可能的 markdown code block 標記
|
|
42
42
|
const jsonContent = content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
|
43
43
|
|
|
44
44
|
try {
|
|
45
45
|
return JSON.parse(jsonContent);
|
|
46
46
|
} catch (error) {
|
|
47
|
-
throw new Error(
|
|
47
|
+
throw new Error(`無法解析 AI 回應為 JSON: ${error.message}`);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* 配置載入器
|
|
3
|
+
* 基於 scripts/commit-modules/config-loader.mjs 和 scripts/ai-pr-modules/core/config-loader.mjs
|
|
4
|
+
* 支援從任何目錄下的 .ai-git-config.mjs 載入配置
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { existsSync } from 'fs';
|
|
8
8
|
import { resolve } from 'path';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* 解析命令行參數
|
|
12
12
|
*/
|
|
13
13
|
export function parseCliArgs() {
|
|
14
14
|
const args = process.argv.slice(2);
|
|
@@ -43,11 +43,11 @@ export function parseCliArgs() {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* 載入配置(commit 工具使用)
|
|
47
|
+
* 支援從目前工作目錄或使用者專案目錄載入 .ai-git-config.mjs
|
|
48
48
|
*/
|
|
49
49
|
export async function loadCommitConfig() {
|
|
50
|
-
//
|
|
50
|
+
// 內建預設值
|
|
51
51
|
const defaults = {
|
|
52
52
|
ai: {
|
|
53
53
|
model: 'gpt-4.1',
|
|
@@ -60,7 +60,7 @@ export async function loadCommitConfig() {
|
|
|
60
60
|
},
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
-
//
|
|
63
|
+
// 嘗試從目前工作目錄載入配置檔案
|
|
64
64
|
const configPath = resolve(process.cwd(), '.ai-git-config.mjs');
|
|
65
65
|
let userConfig = {};
|
|
66
66
|
|
|
@@ -69,11 +69,11 @@ export async function loadCommitConfig() {
|
|
|
69
69
|
const imported = await import(`file://${configPath}`);
|
|
70
70
|
userConfig = imported.default || {};
|
|
71
71
|
} catch (error) {
|
|
72
|
-
console.warn(`⚠️
|
|
72
|
+
console.warn(`⚠️ 載入配置檔案失敗: ${error.message}`);
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
//
|
|
76
|
+
// 合併配置
|
|
77
77
|
const config = {
|
|
78
78
|
ai: {
|
|
79
79
|
model: userConfig.ai?.model ?? defaults.ai.model,
|
|
@@ -86,7 +86,7 @@ export async function loadCommitConfig() {
|
|
|
86
86
|
},
|
|
87
87
|
};
|
|
88
88
|
|
|
89
|
-
//
|
|
89
|
+
// 命令行參數優先
|
|
90
90
|
const cliConfig = parseCliArgs();
|
|
91
91
|
if (cliConfig.model) config.ai.model = cliConfig.model;
|
|
92
92
|
if (cliConfig.verbose) config.output.verbose = cliConfig.verbose;
|
|
@@ -97,7 +97,7 @@ export async function loadCommitConfig() {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
* 解析 PR
|
|
100
|
+
* 解析 PR 命令行參數
|
|
101
101
|
*/
|
|
102
102
|
export function parsePRCliArgs() {
|
|
103
103
|
const args = process.argv.slice(2);
|
|
@@ -154,10 +154,10 @@ export function parsePRCliArgs() {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
|
-
*
|
|
157
|
+
* 載入 PR 配置
|
|
158
158
|
*/
|
|
159
159
|
export async function loadPRConfig() {
|
|
160
|
-
//
|
|
160
|
+
// 嘗試從目前工作目錄載入配置檔案
|
|
161
161
|
const configPath = resolve(process.cwd(), '.ai-git-config.mjs');
|
|
162
162
|
let config = null;
|
|
163
163
|
|
|
@@ -166,11 +166,11 @@ export async function loadPRConfig() {
|
|
|
166
166
|
const userConfig = await import(`file://${configPath}`);
|
|
167
167
|
config = userConfig.default;
|
|
168
168
|
} catch (error) {
|
|
169
|
-
console.warn(`⚠️
|
|
169
|
+
console.warn(`⚠️ 載入配置檔案失敗: ${error.message}`);
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
//
|
|
173
|
+
// 使用預設配置
|
|
174
174
|
if (!config) {
|
|
175
175
|
config = {
|
|
176
176
|
ai: { model: 'gpt-4.1', maxDiffLength: 8000, maxRetries: 3 },
|
|
@@ -180,7 +180,7 @@ export async function loadPRConfig() {
|
|
|
180
180
|
};
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
//
|
|
183
|
+
// 合併命令行參數
|
|
184
184
|
const cliConfig = parsePRCliArgs();
|
|
185
185
|
if (cliConfig.model) config.ai.model = cliConfig.model;
|
|
186
186
|
if (cliConfig.orgName) config.github.orgName = cliConfig.orgName;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Git
|
|
3
|
-
*
|
|
2
|
+
* Git 操作封裝
|
|
3
|
+
* 基於 scripts/ai-pr-modules/core/git-operations.mjs
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { execSync } from 'child_process';
|
|
@@ -10,7 +10,7 @@ const DIFF_CONTEXT_LINES = 50;
|
|
|
10
10
|
|
|
11
11
|
export class GitOperations {
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* 執行 Git 命令
|
|
14
14
|
*/
|
|
15
15
|
static exec(command, options = {}) {
|
|
16
16
|
try {
|
|
@@ -30,7 +30,7 @@ export class GitOperations {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
33
|
+
* 檢查是否在 Git 倉庫中
|
|
34
34
|
*/
|
|
35
35
|
static isGitRepository() {
|
|
36
36
|
try {
|
|
@@ -42,21 +42,21 @@ export class GitOperations {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
45
|
+
* 獲取目前分支
|
|
46
46
|
*/
|
|
47
47
|
static getCurrentBranch() {
|
|
48
48
|
return GitOperations.exec('git rev-parse --abbrev-ref HEAD', { silent: true });
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
52
|
+
* 獲取 staged diff
|
|
53
53
|
*/
|
|
54
54
|
static getStagedDiff() {
|
|
55
55
|
return GitOperations.exec('git diff --staged', { silent: true });
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
59
|
+
* 偵測可用的 release 分支
|
|
60
60
|
*/
|
|
61
61
|
static detectReleaseBranches() {
|
|
62
62
|
try {
|
|
@@ -89,7 +89,7 @@ export class GitOperations {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
*
|
|
92
|
+
* 獲取變更的檔案列表
|
|
93
93
|
*/
|
|
94
94
|
static getChangedFiles(baseBranch, headBranch) {
|
|
95
95
|
try {
|
|
@@ -105,7 +105,7 @@ export class GitOperations {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
*
|
|
108
|
+
* 獲取 commit 列表
|
|
109
109
|
*/
|
|
110
110
|
static getCommits(baseBranch, headBranch) {
|
|
111
111
|
try {
|
|
@@ -113,12 +113,12 @@ export class GitOperations {
|
|
|
113
113
|
encoding: 'utf-8',
|
|
114
114
|
});
|
|
115
115
|
} catch (error) {
|
|
116
|
-
throw new Error(
|
|
116
|
+
throw new Error(`無法比較分支差異: ${error.message}`);
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
|
-
*
|
|
121
|
+
* 獲取 diff
|
|
122
122
|
*/
|
|
123
123
|
static getDiff(baseBranch, headBranch) {
|
|
124
124
|
try {
|
|
@@ -127,21 +127,21 @@ export class GitOperations {
|
|
|
127
127
|
maxBuffer: MAX_BUFFER_SIZE,
|
|
128
128
|
});
|
|
129
129
|
} catch (error) {
|
|
130
|
-
//
|
|
130
|
+
// 嘗試替代方案
|
|
131
131
|
try {
|
|
132
132
|
return execSync(`git diff origin/${baseBranch}..${headBranch}`, {
|
|
133
133
|
encoding: 'utf-8',
|
|
134
134
|
maxBuffer: MAX_BUFFER_SIZE,
|
|
135
135
|
});
|
|
136
136
|
} catch (fallbackError) {
|
|
137
|
-
throw new Error(
|
|
137
|
+
throw new Error(`無法獲取分支差異: ${error.message}`);
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
143
|
+
* 智能截斷 diff
|
|
144
|
+
* 保留前後各 50 行,中間用省略標記
|
|
145
145
|
*/
|
|
146
146
|
static truncateDiff(diff, maxLength = 8000) {
|
|
147
147
|
if (diff.length <= maxLength) return diff;
|
|
@@ -157,23 +157,23 @@ export class GitOperations {
|
|
|
157
157
|
const footer = lines.slice(-contextLines).join('\n');
|
|
158
158
|
const omittedLines = lines.length - contextLines * 2;
|
|
159
159
|
|
|
160
|
-
return `${header}\n\n... [已省略 ${omittedLines}
|
|
160
|
+
return `${header}\n\n... [已省略 ${omittedLines} 行變更] ...\n\n${footer}`;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
/**
|
|
164
|
-
*
|
|
164
|
+
* 推送到遠端
|
|
165
165
|
*/
|
|
166
166
|
static push(branch) {
|
|
167
167
|
try {
|
|
168
168
|
execSync(`git push -u origin ${branch}`, { stdio: 'inherit' });
|
|
169
169
|
return true;
|
|
170
170
|
} catch (error) {
|
|
171
|
-
throw new Error(
|
|
171
|
+
throw new Error(`推送失敗: ${error.message}`);
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
/**
|
|
176
|
-
*
|
|
176
|
+
* 同步遠端資訊
|
|
177
177
|
*/
|
|
178
178
|
static fetch() {
|
|
179
179
|
try {
|
|
@@ -185,7 +185,7 @@ export class GitOperations {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
/**
|
|
188
|
-
*
|
|
188
|
+
* 獲取變更統計
|
|
189
189
|
*/
|
|
190
190
|
static getChangeStats(baseBranch, headBranch) {
|
|
191
191
|
try {
|
|
@@ -200,7 +200,7 @@ export class GitOperations {
|
|
|
200
200
|
|
|
201
201
|
return { stats, filesChanged: parseInt(filesChanged, 10) };
|
|
202
202
|
} catch (error) {
|
|
203
|
-
return { stats: '
|
|
203
|
+
return { stats: '無法獲取統計', filesChanged: 0 };
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
}
|
package/src/utils/helpers.js
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Helper
|
|
3
|
-
*
|
|
2
|
+
* Helper 工具函式
|
|
3
|
+
* 基於 scripts/ai-pr-modules/utils/helpers.mjs
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* 清理 commit message
|
|
8
|
-
* 移除 markdown
|
|
8
|
+
* 移除 markdown 程式碼區塊標記
|
|
9
9
|
*/
|
|
10
10
|
export function cleanCommitMessage(message) {
|
|
11
11
|
if (!message) return '';
|
|
12
12
|
|
|
13
13
|
let cleaned = message.trim();
|
|
14
14
|
|
|
15
|
-
// 移除 markdown
|
|
15
|
+
// 移除 markdown 程式碼區塊標記
|
|
16
16
|
cleaned = cleaned.replace(/^```[\w]*\n/gm, '');
|
|
17
17
|
cleaned = cleaned.replace(/\n```$/gm, '');
|
|
18
18
|
cleaned = cleaned.replace(/^```$/gm, '');
|
|
19
19
|
|
|
20
|
-
//
|
|
20
|
+
// 移除開頭和結尾的引號
|
|
21
21
|
cleaned = cleaned.replace(/^["']|["']$/g, '');
|
|
22
22
|
|
|
23
23
|
return cleaned.trim();
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
27
|
+
* 驗證 commit message
|
|
28
28
|
*/
|
|
29
29
|
export function validateCommitMessage(message) {
|
|
30
30
|
if (!message || message.length === 0) {
|
|
31
|
-
return { valid: false, reason: 'Commit message
|
|
31
|
+
return { valid: false, reason: 'Commit message 為空' };
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
if (message.length < 5) {
|
|
35
|
-
return { valid: false, reason: 'Commit message
|
|
35
|
+
return { valid: false, reason: 'Commit message 太短(少於 5 個字元)' };
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
//
|
|
38
|
+
// 檢查是否只包含特殊字元或空白
|
|
39
39
|
if (!/[a-zA-Z0-9\u4e00-\u9fa5]/.test(message)) {
|
|
40
40
|
return { valid: false, reason: 'Commit message 不包含有效字元' };
|
|
41
41
|
}
|
|
@@ -44,26 +44,26 @@ export function validateCommitMessage(message) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
47
|
+
* 錯誤處理
|
|
48
48
|
*/
|
|
49
49
|
export function handleError(error) {
|
|
50
|
-
console.error('\n❌
|
|
50
|
+
console.error('\n❌ 錯誤:', error.message);
|
|
51
51
|
|
|
52
52
|
if (error.suggestions && error.suggestions.length > 0) {
|
|
53
|
-
console.log('\n💡
|
|
53
|
+
console.log('\n💡 建議解決方案:');
|
|
54
54
|
error.suggestions.forEach((suggestion, index) => {
|
|
55
55
|
console.log(` ${index + 1}. ${suggestion}`);
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
if (error.stack && process.env.VERBOSE) {
|
|
60
|
-
console.error('\n
|
|
60
|
+
console.error('\n堆疊追蹤:');
|
|
61
61
|
console.error(error.stack);
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
66
|
+
* 獲取專案類型提示(用於 AI prompt)
|
|
67
67
|
*/
|
|
68
68
|
export function getProjectTypePrompt() {
|
|
69
69
|
return `你是一個資深前端工程師,熟悉 Next.js 專案的開發規範。
|