ai-git-tools 1.0.5 → 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 +24 -45
- package/package.json +1 -1
- package/src/commands/commit-all.js +224 -163
- package/src/commands/commit.js +87 -71
- package/src/commands/init.js +32 -130
- package/src/commands/pr.js +120 -236
- package/src/core/ai-client.js +18 -83
- package/src/core/config-loader.js +164 -132
- package/src/core/git-operations.js +83 -194
- package/src/utils/helpers.js +39 -50
- package/src/utils/logger.js +28 -100
- package/src/commands/workflow.js +0 -36
- package/src/core/github-api.js +0 -139
- package/src/index.js +0 -16
package/bin/cli.js
CHANGED
|
@@ -4,14 +4,13 @@
|
|
|
4
4
|
* AI Git Tools CLI
|
|
5
5
|
*
|
|
6
6
|
* AI-powered Git automation for commit messages and PR generation
|
|
7
|
+
* 完全重写版本基於 scripts/ 原始实现
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import { Command } from 'commander';
|
|
10
|
-
import chalk from 'chalk';
|
|
11
11
|
import { commitCommand } from '../src/commands/commit.js';
|
|
12
12
|
import { commitAllCommand } from '../src/commands/commit-all.js';
|
|
13
13
|
import { prCommand } from '../src/commands/pr.js';
|
|
14
|
-
import { workflowCommand } from '../src/commands/workflow.js';
|
|
15
14
|
import { initCommand } from '../src/commands/init.js';
|
|
16
15
|
|
|
17
16
|
const program = new Command();
|
|
@@ -19,68 +18,48 @@ const program = new Command();
|
|
|
19
18
|
program
|
|
20
19
|
.name('ai-git-tools')
|
|
21
20
|
.description('AI-powered Git automation tools')
|
|
22
|
-
.version('
|
|
21
|
+
.version('2.0.0');
|
|
23
22
|
|
|
24
23
|
// Init 命令
|
|
25
24
|
program
|
|
26
25
|
.command('init')
|
|
27
|
-
.description('初始化配置檔案')
|
|
26
|
+
.description('初始化配置檔案 (.ai-git-config.mjs)')
|
|
28
27
|
.action(initCommand);
|
|
29
28
|
|
|
30
29
|
// Commit 命令
|
|
31
30
|
program
|
|
32
31
|
.command('commit')
|
|
33
|
-
.description('
|
|
34
|
-
.option('
|
|
35
|
-
.option('-v, --verbose', '
|
|
36
|
-
.option('--max-diff <number>', '最大 diff
|
|
37
|
-
.option('--max-retries <number>', '
|
|
32
|
+
.description('AI 自动生成 commit message 并提交')
|
|
33
|
+
.option('--model <model>', '指定 AI 模型')
|
|
34
|
+
.option('-v, --verbose', '顯示详细输出')
|
|
35
|
+
.option('--max-diff <number>', '最大 diff 长度')
|
|
36
|
+
.option('--max-retries <number>', '最大重试次数')
|
|
38
37
|
.action(commitCommand);
|
|
39
38
|
|
|
40
39
|
// Commit All 命令
|
|
41
40
|
program
|
|
42
41
|
.command('commit-all')
|
|
43
|
-
.
|
|
44
|
-
.
|
|
45
|
-
.option('-
|
|
46
|
-
.option('-
|
|
47
|
-
.option('--max-
|
|
48
|
-
.option('--max-retries <number>', '最大重試次數', parseInt)
|
|
42
|
+
.description('智慧分析所有變更并自动分組提交')
|
|
43
|
+
.option('--model <model>', '指定 AI 模型')
|
|
44
|
+
.option('-v, --verbose', '顯示详细输出')
|
|
45
|
+
.option('--max-diff <number>', '最大 diff 长度')
|
|
46
|
+
.option('--max-retries <number>', '最大重试次数')
|
|
49
47
|
.action(commitAllCommand);
|
|
50
48
|
|
|
51
49
|
// PR 命令
|
|
52
50
|
program
|
|
53
51
|
.command('pr')
|
|
54
|
-
.description('
|
|
55
|
-
.option('
|
|
56
|
-
.option('
|
|
57
|
-
.option('
|
|
58
|
-
.option('--
|
|
59
|
-
.option('--
|
|
60
|
-
.option('--
|
|
61
|
-
.option('--
|
|
62
|
-
.option('--auto-
|
|
52
|
+
.description('AI 自动生成 PR 并创建 Pull Request')
|
|
53
|
+
.option('--base <branch>', '指定目标分支')
|
|
54
|
+
.option('--head <branch>', '指定来源分支')
|
|
55
|
+
.option('--model <model>', '指定 AI 模型')
|
|
56
|
+
.option('--org <org-name>', '指定 GitHub 組织名称')
|
|
57
|
+
.option('--draft', '创建草稿 PR')
|
|
58
|
+
.option('--preview', '仅预览 PR 内容,不实际创建')
|
|
59
|
+
.option('--no-confirm', '跳過确认直接创建')
|
|
60
|
+
.option('--auto-reviewers', '自动选择 reviewers')
|
|
61
|
+
.option('--auto-labels', '自动添加 Labels')
|
|
63
62
|
.option('--no-labels', '不添加 Labels')
|
|
64
|
-
.option('--org <name>', 'GitHub 組織名稱')
|
|
65
63
|
.action(prCommand);
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
program
|
|
69
|
-
.command('workflow')
|
|
70
|
-
.alias('wf')
|
|
71
|
-
.description('完整工作流程:commit-all + pr')
|
|
72
|
-
.option('-m, --model <model>', '指定 AI 模型')
|
|
73
|
-
.option('-v, --verbose', '顯示詳細輸出')
|
|
74
|
-
.option('-b, --base <branch>', 'PR 目標分支')
|
|
75
|
-
.option('--draft', '創建草稿 PR')
|
|
76
|
-
.option('--auto-reviewers', '自動選擇 reviewers')
|
|
77
|
-
.option('--auto-labels', '自動添加 Labels')
|
|
78
|
-
.action(workflowCommand);
|
|
79
|
-
|
|
80
|
-
// 解析命令列參數
|
|
81
|
-
program.parse(process.argv);
|
|
82
|
-
|
|
83
|
-
// 如果沒有提供命令,顯示幫助
|
|
84
|
-
if (!process.argv.slice(2).length) {
|
|
85
|
-
program.outputHelp();
|
|
86
|
-
}
|
|
65
|
+
program.parse();
|
package/package.json
CHANGED
|
@@ -1,34 +1,101 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Commit All 命令
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* 基於 scripts/ai-auto-commit-all.mjs
|
|
4
|
+
* 智慧分析所有變更並自動分類提交
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import { readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
9
|
+
import { loadCommitConfig } from '../core/config-loader.js';
|
|
10
10
|
import { AIClient } from '../core/ai-client.js';
|
|
11
|
-
import { GitOperations } from '../core/git-operations.js';
|
|
12
11
|
import { Logger } from '../utils/logger.js';
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
import { handleError } from '../utils/helpers.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 獲取檔案的變更內容
|
|
16
|
+
*/
|
|
17
|
+
function getFileDiff(filePath, isNew) {
|
|
18
|
+
try {
|
|
19
|
+
if (isNew) {
|
|
20
|
+
// 新檔案:讀取完整內容(前 100 行)
|
|
21
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
22
|
+
const lines = content.split('\n').slice(0, 100);
|
|
23
|
+
return `[新檔案]\n${lines.join('\n')}${lines.length >= 100 ? '\n...' : ''}`;
|
|
24
|
+
}
|
|
25
|
+
// 已存在檔案:獲取 diff
|
|
26
|
+
const diff = execSync(`git diff HEAD -- "${filePath}"`, {
|
|
27
|
+
encoding: 'utf-8',
|
|
28
|
+
}).toString();
|
|
29
|
+
return diff || '[無變更]';
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return `[讀取錯誤: ${error.message}]`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 獲取所有未提交的變更
|
|
37
|
+
*/
|
|
38
|
+
function getAllChanges() {
|
|
39
|
+
try {
|
|
40
|
+
const status = execSync('git status --porcelain', {
|
|
41
|
+
encoding: 'utf-8',
|
|
42
|
+
}).toString();
|
|
43
|
+
|
|
44
|
+
if (!status.trim()) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const changes = [];
|
|
49
|
+
const lines = status.split('\n').filter((line) => line.trim());
|
|
50
|
+
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
const statusCode = line.substring(0, 2);
|
|
53
|
+
const filePath = line.substring(3).trim();
|
|
54
|
+
|
|
55
|
+
// 跳過已刪除的檔案
|
|
56
|
+
if (statusCode.includes('D')) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 跳過某些不需要提交的檔案
|
|
61
|
+
if (
|
|
62
|
+
filePath.includes('node_modules/') ||
|
|
63
|
+
filePath.includes('.next/') ||
|
|
64
|
+
filePath.includes('dist/') ||
|
|
65
|
+
filePath.includes('.DS_Store')
|
|
66
|
+
) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const isNew = statusCode.includes('?') || statusCode.includes('A');
|
|
71
|
+
const isStaged = statusCode[0] !== ' ' && statusCode[0] !== '?';
|
|
72
|
+
|
|
73
|
+
changes.push({
|
|
74
|
+
filePath,
|
|
75
|
+
isNew,
|
|
76
|
+
isStaged,
|
|
77
|
+
statusCode,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return changes;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('獲取變更列表失敗:', error.message);
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
20
87
|
|
|
21
88
|
/**
|
|
22
89
|
* 使用 AI 分析並分組變更
|
|
23
90
|
*/
|
|
24
|
-
async function analyzeAndGroupChanges(changes, config
|
|
25
|
-
|
|
91
|
+
async function analyzeAndGroupChanges(changes, config) {
|
|
92
|
+
console.log('🤖 正在使用 AI 分析變更並分組...\n');
|
|
26
93
|
|
|
27
94
|
// 準備變更摘要
|
|
28
95
|
const maxDiffPerFile = Math.floor(config.ai.maxDiffLength / Math.max(changes.length, 1));
|
|
29
96
|
const changeSummary = changes
|
|
30
97
|
.map((change, index) => {
|
|
31
|
-
const diff =
|
|
98
|
+
const diff = getFileDiff(change.filePath, change.isNew);
|
|
32
99
|
const lines = diff.split('\n');
|
|
33
100
|
const truncatedDiff = lines.slice(0, Math.min(50, maxDiffPerFile / 100)).join('\n');
|
|
34
101
|
return `[檔案 ${index}] ${change.filePath}\n${
|
|
@@ -37,27 +104,36 @@ async function analyzeAndGroupChanges(changes, config, logger) {
|
|
|
37
104
|
})
|
|
38
105
|
.join('\n---\n\n');
|
|
39
106
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
|
|
107
|
+
const prompt = `你是一個資深前端工程師,熟悉 Next.js 專案的開發規範。請分析以下的檔案變更,並將它們按照功能/目的分組。
|
|
108
|
+
|
|
109
|
+
**專案背景**:
|
|
110
|
+
- Next.js 12+ (Pages Router)
|
|
111
|
+
- TypeScript + JavaScript 混合
|
|
112
|
+
- Tailwind CSS + Styled Components
|
|
113
|
+
- Zustand (客戶端狀態) + SWR (伺服器資料獲取)
|
|
114
|
+
- React Hook Form + Zod (表單處理)
|
|
115
|
+
- 架构:Modified Atomic Design(UI / Page / Feature 三层)
|
|
116
|
+
|
|
117
|
+
**專案目錄結構參考**:
|
|
118
|
+
- pages/ → 頁面路由
|
|
119
|
+
- components/Page/ → 頁面級元件
|
|
120
|
+
- components/UI/ 或 components/Common/ → 共用 UI 元件
|
|
121
|
+
- components/[Feature]/ → 功能模組元件
|
|
122
|
+
- store/ → Zustand 狀態管理
|
|
123
|
+
- api/ → API 呼叫
|
|
124
|
+
- utils/ → 工具函式
|
|
125
|
+
- styles/ → 全域樣式
|
|
50
126
|
|
|
51
|
-
|
|
127
|
+
規則:
|
|
52
128
|
1. 將相關功能的變更歸類在同一組(例如:同一個功能開發、同一個 bug 修復、相關的重構等)
|
|
53
129
|
2. 每組應該要有明確的主題
|
|
54
|
-
3. 同一個功能的元件、API、樣式應歸為同一組
|
|
55
|
-
4. 設定檔(config
|
|
130
|
+
3. 同一個功能的元件、API、store、樣式應歸為同一組
|
|
131
|
+
4. 設定檔(config)和檔案(docs)變更可以獨立成一組
|
|
56
132
|
5. 輸出格式為 JSON 陣列,每個元素包含:
|
|
57
133
|
- group_name: 群組名稱(簡短描述,繁體中文)
|
|
58
134
|
- commit_type: commit 類型(feat/fix/docs/style/refactor/test/chore/perf)
|
|
59
|
-
- commit_scope: commit 影響範圍(如 api、ui、config
|
|
60
|
-
- file_indices:
|
|
135
|
+
- commit_scope: commit 影響範圍(如 member、report、auth、api、ui、config)
|
|
136
|
+
- file_indices: 屬於這組的檔案索引陣列(對應上面的 [檔案 X])
|
|
61
137
|
- description: 這組變更的詳細說明(繁體中文)
|
|
62
138
|
|
|
63
139
|
範例輸出:
|
|
@@ -83,234 +159,219 @@ ${changeSummary}
|
|
|
83
159
|
|
|
84
160
|
請只輸出 JSON,不要其他文字。`;
|
|
85
161
|
|
|
86
|
-
const response = await
|
|
87
|
-
await aiClient.stop();
|
|
162
|
+
const response = await AIClient.sendAndWait(prompt, config.ai.model);
|
|
88
163
|
|
|
89
164
|
try {
|
|
90
|
-
|
|
91
|
-
logger.succeedSpinner(`AI 分析完成,共分為 ${groups.length} 個群組`);
|
|
92
|
-
return groups;
|
|
165
|
+
return AIClient.parseJSON(response);
|
|
93
166
|
} catch (error) {
|
|
94
|
-
|
|
95
|
-
|
|
167
|
+
console.error('❌ 無法解析 AI 回應:', error.message);
|
|
168
|
+
console.log('原始回應:', response);
|
|
169
|
+
return null;
|
|
96
170
|
}
|
|
97
171
|
}
|
|
98
172
|
|
|
99
173
|
/**
|
|
100
|
-
*
|
|
174
|
+
* 为特定群組生成 commit message
|
|
101
175
|
*/
|
|
102
|
-
async function generateCommitMessage(group, files, config
|
|
176
|
+
async function generateCommitMessage(group, files, config) {
|
|
103
177
|
const filesList = files
|
|
104
|
-
.map(file => {
|
|
105
|
-
const diff =
|
|
178
|
+
.map((file) => {
|
|
179
|
+
const diff = getFileDiff(file.filePath, file.isNew);
|
|
106
180
|
return `檔案: ${file.filePath}\n${diff}`;
|
|
107
181
|
})
|
|
108
182
|
.join('\n\n---\n\n');
|
|
109
183
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
const prompt = `請根據以下資訊生成一則 commit message:
|
|
184
|
+
const prompt = `请根据以下资讯生成一则 commit message:
|
|
113
185
|
|
|
114
|
-
|
|
115
|
-
Commit
|
|
116
|
-
Commit
|
|
117
|
-
|
|
186
|
+
群組名称: ${group.group_name}
|
|
187
|
+
Commit 类型: ${group.commit_type}
|
|
188
|
+
Commit 范围: ${group.commit_scope || '未指定'}
|
|
189
|
+
说明: ${group.description}
|
|
118
190
|
|
|
119
191
|
檔案變更:
|
|
120
192
|
${filesList}
|
|
121
193
|
|
|
122
|
-
|
|
194
|
+
规则:
|
|
123
195
|
- 使用 Conventional Commits 格式:${group.commit_type}${
|
|
124
196
|
group.commit_scope ? `(${group.commit_scope})` : ''
|
|
125
197
|
}: <subject>
|
|
126
|
-
- subject 限制在 50
|
|
198
|
+
- subject 限制在 50 字内,使用繁体中文
|
|
127
199
|
- 如果變更複雜,可以加上 body(用空行分隔),body 使用 bullet points
|
|
128
|
-
-
|
|
129
|
-
- 不要包含 markdown code block
|
|
130
|
-
-
|
|
200
|
+
- 只输出 commit message 本身,不要其他说明
|
|
201
|
+
- 不要包含 markdown code block 标记(不要 \`\`\`)
|
|
202
|
+
- 不要加上任何引导语句
|
|
131
203
|
|
|
132
|
-
|
|
204
|
+
输出格式范例:
|
|
133
205
|
feat(auth): 新增使用者登入功能
|
|
134
206
|
|
|
135
|
-
-
|
|
136
|
-
-
|
|
137
|
-
- 整合 JWT
|
|
207
|
+
- 实作登入 API endpoint
|
|
208
|
+
- 新增登入页面 UI
|
|
209
|
+
- 整合 JWT 认证机制`;
|
|
210
|
+
|
|
211
|
+
const response = await AIClient.sendAndWait(prompt, config.ai.model);
|
|
138
212
|
|
|
139
|
-
|
|
140
|
-
|
|
213
|
+
// 清理可能的 markdown code block 标记
|
|
214
|
+
let commitMessage = response.trim();
|
|
215
|
+
commitMessage = commitMessage
|
|
216
|
+
.replace(/^```[\s\S]*?\n/, '')
|
|
217
|
+
.replace(/\n```$/, '')
|
|
218
|
+
.trim();
|
|
141
219
|
|
|
142
|
-
return
|
|
220
|
+
return commitMessage;
|
|
143
221
|
}
|
|
144
222
|
|
|
145
223
|
/**
|
|
146
224
|
* 執行分組提交
|
|
147
225
|
*/
|
|
148
|
-
async function commitGroup(group, files, config
|
|
226
|
+
async function commitGroup(group, files, config) {
|
|
149
227
|
try {
|
|
150
|
-
console.log(
|
|
151
|
-
console.log(
|
|
152
|
-
|
|
228
|
+
console.log(`\n📦 处理群組: ${group.group_name}`);
|
|
229
|
+
console.log(
|
|
230
|
+
` 类型: ${group.commit_type}${group.commit_scope ? `(${group.commit_scope})` : ''}`
|
|
231
|
+
);
|
|
232
|
+
console.log(` 檔案数量: ${files.length}`);
|
|
153
233
|
|
|
154
|
-
//
|
|
155
|
-
|
|
234
|
+
// 先 reset 所有已 staged 的檔案
|
|
235
|
+
try {
|
|
236
|
+
execSync('git reset HEAD -- .', { stdio: 'ignore' });
|
|
237
|
+
} catch (e) {
|
|
238
|
+
// 忽略錯誤(可能没有 staged 的檔案)
|
|
239
|
+
}
|
|
156
240
|
|
|
157
|
-
// Add
|
|
158
|
-
const addedFiles = [];
|
|
241
|
+
// Add 这組的檔案
|
|
159
242
|
for (const file of files) {
|
|
160
243
|
console.log(` ├─ ${file.filePath}`);
|
|
161
244
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// 繼續處理其他檔案,不要中斷
|
|
167
|
-
continue;
|
|
245
|
+
execSync(`git add "${file.filePath}"`, { encoding: 'utf-8' });
|
|
246
|
+
} catch (addError) {
|
|
247
|
+
console.error(` ⚠️ 無法加入檔案: ${file.filePath}`, addError.message);
|
|
248
|
+
throw addError;
|
|
168
249
|
}
|
|
169
250
|
}
|
|
170
251
|
|
|
171
|
-
// 如果沒有成功 add 任何檔案,跳過這個群組
|
|
172
|
-
if (addedFiles.length === 0) {
|
|
173
|
-
logger.warn('沒有檔案被成功 add,跳過此群組');
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
252
|
// 生成 commit message
|
|
178
253
|
console.log(` └─ 生成 commit message...`);
|
|
179
|
-
const commitMessage = await generateCommitMessage(group, files, config
|
|
254
|
+
const commitMessage = await generateCommitMessage(group, files, config);
|
|
180
255
|
|
|
181
256
|
if (!commitMessage) {
|
|
182
|
-
|
|
183
|
-
return false;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// 驗證 commit message
|
|
187
|
-
const validation = validateCommitMessage(commitMessage);
|
|
188
|
-
if (!validation.valid) {
|
|
189
|
-
logger.warn(`Commit message 無效(${validation.reason}),跳過此群組`);
|
|
257
|
+
console.log(` ❌ 無法生成 commit message,跳過此群組`);
|
|
190
258
|
return false;
|
|
191
259
|
}
|
|
192
260
|
|
|
193
|
-
console.log(
|
|
194
|
-
|
|
261
|
+
console.log(`\n 📝 Commit Message:`);
|
|
262
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
263
|
+
commitMessage.split('\n').forEach((line) => {
|
|
264
|
+
console.log(` ${line}`);
|
|
265
|
+
});
|
|
266
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
195
267
|
|
|
196
268
|
// 執行 commit
|
|
197
|
-
|
|
198
|
-
|
|
269
|
+
// 使用暫存檔案避免 commit message 中的特殊字元問題
|
|
270
|
+
const tmpFile = '.git/COMMIT_EDITMSG_TMP';
|
|
271
|
+
try {
|
|
272
|
+
writeFileSync(tmpFile, commitMessage, 'utf-8');
|
|
273
|
+
execSync(`git commit -F ${tmpFile}`, {
|
|
274
|
+
stdio: 'inherit',
|
|
275
|
+
});
|
|
276
|
+
unlinkSync(tmpFile);
|
|
277
|
+
} catch (commitError) {
|
|
278
|
+
try {
|
|
279
|
+
unlinkSync(tmpFile);
|
|
280
|
+
} catch (e) {
|
|
281
|
+
// 忽略刪除临时檔案的錯誤
|
|
282
|
+
}
|
|
283
|
+
throw commitError;
|
|
284
|
+
}
|
|
199
285
|
|
|
286
|
+
console.log(` ✅ Commit 完成!`);
|
|
200
287
|
return true;
|
|
201
288
|
} catch (error) {
|
|
202
|
-
|
|
289
|
+
console.error(` ❌ Commit 失敗:`, error.message);
|
|
203
290
|
return false;
|
|
204
291
|
}
|
|
205
292
|
}
|
|
206
293
|
|
|
207
294
|
/**
|
|
208
|
-
* Commit All
|
|
295
|
+
* Commit All 命令主函数
|
|
209
296
|
*/
|
|
210
|
-
export async function commitAllCommand(
|
|
211
|
-
const logger = new Logger(
|
|
297
|
+
export async function commitAllCommand() {
|
|
298
|
+
const logger = new Logger();
|
|
212
299
|
|
|
213
300
|
try {
|
|
214
|
-
logger.header('智能分析所有變更並自動提交');
|
|
215
|
-
|
|
216
|
-
// 檢查是否在 Git 倉庫中
|
|
217
|
-
if (!GitOperations.isGitRepository()) {
|
|
218
|
-
logger.error('當前目錄不是 Git 倉庫');
|
|
219
|
-
process.exit(1);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
301
|
// 載入配置
|
|
223
|
-
const config = await
|
|
302
|
+
const config = await loadCommitConfig();
|
|
303
|
+
|
|
304
|
+
logger.header('智慧分析所有變更並自動提交');
|
|
224
305
|
|
|
225
306
|
if (config.output.verbose) {
|
|
226
|
-
|
|
227
|
-
|
|
307
|
+
console.log('📋 使用配置:');
|
|
308
|
+
console.log(` AI Model: ${config.ai.model}`);
|
|
309
|
+
console.log(` Max Diff Length: ${config.ai.maxDiffLength}`);
|
|
310
|
+
console.log(` Max Retries: ${config.ai.maxRetries}`);
|
|
311
|
+
console.log('');
|
|
228
312
|
}
|
|
229
313
|
|
|
230
|
-
// 獲取所有變更
|
|
231
|
-
logger.
|
|
232
|
-
const changes =
|
|
233
|
-
logger.succeedSpinner(`找到 ${changes.length} 個變更的檔案`);
|
|
314
|
+
// 1. 獲取所有變更
|
|
315
|
+
logger.step('掃描變更中...');
|
|
316
|
+
const changes = getAllChanges();
|
|
234
317
|
|
|
235
318
|
if (changes.length === 0) {
|
|
236
319
|
logger.info('沒有需要提交的變更');
|
|
237
320
|
process.exit(0);
|
|
238
321
|
}
|
|
239
322
|
|
|
240
|
-
console.log(
|
|
241
|
-
|
|
323
|
+
console.log(`📊 找到 ${changes.length} 个變更的檔案:\n`);
|
|
324
|
+
changes.forEach((change, index) => {
|
|
325
|
+
const status = change.isNew ? '新增' : '修改';
|
|
326
|
+
console.log(` [${index}] ${status} - ${change.filePath}`);
|
|
327
|
+
});
|
|
328
|
+
console.log();
|
|
242
329
|
|
|
243
|
-
// 使用 AI 分析並分組
|
|
244
|
-
const groups = await analyzeAndGroupChanges(changes, config
|
|
330
|
+
// 2. 使用 AI 分析並分組
|
|
331
|
+
const groups = await analyzeAndGroupChanges(changes, config);
|
|
245
332
|
|
|
246
333
|
if (!groups || groups.length === 0) {
|
|
247
334
|
logger.error('AI 分析失敗或沒有產生分組');
|
|
248
335
|
process.exit(1);
|
|
249
336
|
}
|
|
250
337
|
|
|
251
|
-
|
|
338
|
+
logger.success(`AI 分析完成,共分為 ${groups.length} 個群組:\n`);
|
|
252
339
|
groups.forEach((group, index) => {
|
|
253
340
|
console.log(` 群組 ${index + 1}: ${group.group_name} (${group.commit_type})`);
|
|
254
|
-
console.log(` └─ 包含 ${group.file_indices.length}
|
|
341
|
+
console.log(` └─ 包含 ${group.file_indices.length} 个檔案`);
|
|
255
342
|
});
|
|
256
343
|
|
|
257
|
-
//
|
|
258
|
-
logger.
|
|
344
|
+
// 3. 依序提交每个群組
|
|
345
|
+
logger.separator('=', 60);
|
|
346
|
+
console.log('開始執行提交...');
|
|
347
|
+
logger.separator('=', 60);
|
|
259
348
|
|
|
260
349
|
let successCount = 0;
|
|
261
350
|
for (let i = 0; i < groups.length; i++) {
|
|
262
351
|
const group = groups[i];
|
|
263
|
-
|
|
264
|
-
// 驗證並過濾有效的檔案索引
|
|
265
|
-
const validFiles = group.file_indices
|
|
266
|
-
.filter(index => {
|
|
267
|
-
if (typeof index !== 'number' || index < 0 || index >= changes.length) {
|
|
268
|
-
logger.warn(`群組 "${group.group_name}" 包含無效的檔案索引: ${index} (有效範圍: 0-${changes.length - 1})`);
|
|
269
|
-
return false;
|
|
270
|
-
}
|
|
271
|
-
return true;
|
|
272
|
-
})
|
|
273
|
-
.map(index => changes[index])
|
|
274
|
-
.filter(file => {
|
|
275
|
-
if (!file || !file.filePath) {
|
|
276
|
-
logger.warn(`檔案資訊不完整,已跳過`);
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
if (!existsSync(file.filePath)) {
|
|
280
|
-
logger.warn(`檔案不存在:${file.filePath},已跳過`);
|
|
281
|
-
return false;
|
|
282
|
-
}
|
|
283
|
-
return true;
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
if (validFiles.length === 0) {
|
|
287
|
-
logger.warn(`群組 ${i + 1} 沒有有效的檔案,跳過`);
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
352
|
+
const groupFiles = group.file_indices.map((index) => changes[index]);
|
|
290
353
|
|
|
291
|
-
const success = await commitGroup(group,
|
|
354
|
+
const success = await commitGroup(group, groupFiles, config);
|
|
292
355
|
if (success) {
|
|
293
356
|
successCount++;
|
|
294
357
|
}
|
|
295
358
|
}
|
|
296
359
|
|
|
297
|
-
// 顯示摘要
|
|
298
|
-
logger.
|
|
360
|
+
// 4. 顯示摘要
|
|
361
|
+
logger.separator('=', 60);
|
|
362
|
+
logger.success(`完成!成功提交 ${successCount}/${groups.length} 个群組`);
|
|
363
|
+
logger.separator('=', 60);
|
|
299
364
|
|
|
300
|
-
//
|
|
301
|
-
console.log(
|
|
302
|
-
|
|
303
|
-
const recentCommits = GitOperations.getRecentCommits(successCount);
|
|
304
|
-
if (recentCommits) {
|
|
305
|
-
console.log(recentCommits);
|
|
306
|
-
}
|
|
307
|
-
} catch (error) {
|
|
308
|
-
// 忽略顯示 commit 的錯誤
|
|
309
|
-
}
|
|
365
|
+
// 顯示最近的几个 commits
|
|
366
|
+
console.log('\n📋 最近的 commits:');
|
|
367
|
+
execSync(`git log -${successCount} --oneline`, { stdio: 'inherit' });
|
|
310
368
|
|
|
311
369
|
// Reset 任何剩餘的 staged 檔案
|
|
312
|
-
|
|
313
|
-
|
|
370
|
+
try {
|
|
371
|
+
execSync('git reset HEAD -- .', { stdio: 'ignore' });
|
|
372
|
+
} catch (e) {
|
|
373
|
+
// 忽略
|
|
374
|
+
}
|
|
314
375
|
} catch (error) {
|
|
315
376
|
handleError(error);
|
|
316
377
|
process.exit(1);
|