ai-git-tools 1.0.5 → 2.0.0

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.
@@ -1,316 +1,377 @@
1
1
  /**
2
2
  * Commit All 命令
3
- *
4
- * 智能分析所有變更並自動分組提交
3
+ * 基于 scripts/ai-auto-commit-all.mjs
4
+ * 智能分析所有变更并自动分类提交
5
5
  */
6
6
 
7
- import chalk from 'chalk';
8
- import { existsSync } from 'fs';
9
- import { loadConfig } from '../core/config-loader.js';
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
- handleError,
15
- validateCommitMessage,
16
- formatFileList,
17
- getProjectTypePrompt,
18
- getConventionalCommitsRules,
19
- } from '../utils/helpers.js';
12
+ import { handleError } from '../utils/helpers.js';
20
13
 
21
14
  /**
22
- * 使用 AI 分析並分組變更
15
+ * 获取文件的变更内容
23
16
  */
24
- async function analyzeAndGroupChanges(changes, config, logger) {
25
- logger.startSpinner('AI 正在分析變更並分組...');
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
+ }
26
69
 
27
- // 準備變更摘要
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
+ }
87
+
88
+ /**
89
+ * 使用 AI 分析并分组变更
90
+ */
91
+ async function analyzeAndGroupChanges(changes, config) {
92
+ console.log('🤖 正在使用 AI 分析变更并分组...\n');
93
+
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 = GitOperations.getFileDiff(change.filePath, change.isNew);
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
- return `[檔案 ${index}] ${change.filePath}\n${
35
- change.isNew ? '(新檔案)' : '(已修改)'
101
+ return `[文件 ${index}] ${change.filePath}\n${
102
+ change.isNew ? '(新文件)' : '(已修改)'
36
103
  }\n${truncatedDiff}\n`;
37
104
  })
38
105
  .join('\n---\n\n');
39
106
 
40
- const aiClient = new AIClient(config);
41
-
42
- const prompt = `${getProjectTypePrompt()}
43
-
44
- 請分析以下的檔案變更,並將它們按照功能/目的分組。
45
-
46
- **重要提醒**:
47
- - 每個檔案都有一個 [檔案 X] 的標記,X 是索引號碼
48
- - 你**必須**使用這些索引號碼,**不要**自行推測或修改檔案名稱
49
- - file_indices 中只能包含上面提供的有效索引 (0 到 ${changes.length - 1})
50
-
51
- **分組規則**:
52
- 1. 將相關功能的變更歸類在同一組(例如:同一個功能開發、同一個 bug 修復、相關的重構等)
53
- 2. 每組應該要有明確的主題
54
- 3. 同一個功能的元件、API、樣式應歸為同一組
55
- 4. 設定檔(config)和文件(docs)變更可以獨立成一組
56
- 5. 輸出格式為 JSON 陣列,每個元素包含:
57
- - group_name: 群組名稱(簡短描述,繁體中文)
58
- - commit_type: commit 類型(feat/fix/docs/style/refactor/test/chore/perf)
59
- - commit_scope: commit 影響範圍(如 api、ui、config、auth 等,選填)
60
- - file_indices: 屬於這組的檔案索引陣列(**只能使用上面 [檔案 X] 的 X 值**)
61
- - description: 這組變更的詳細說明(繁體中文)
62
-
63
- 範例輸出:
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/ 全域样式
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
+ 范例输出:
64
140
  [
65
141
  {
66
142
  "group_name": "新增使用者登入功能",
67
143
  "commit_type": "feat",
68
144
  "commit_scope": "auth",
69
145
  "file_indices": [0, 1, 2],
70
- "description": "實作使用者登入 API 和前端頁面"
146
+ "description": "实作使用者登入 API 和前端页面"
71
147
  },
72
148
  {
73
- "group_name": "修正導航列手機版顯示",
149
+ "group_name": "修正导航列手机版显示",
74
150
  "commit_type": "fix",
75
151
  "commit_scope": "ui",
76
152
  "file_indices": [3, 4],
77
- "description": "修正導航列在手機版的顯示問題"
153
+ "description": "修正导航列在手机版的显示问题"
78
154
  }
79
155
  ]
80
156
 
81
- 檔案變更內容:
157
+ 文件变更内容:
82
158
  ${changeSummary}
83
159
 
84
- 請只輸出 JSON,不要其他文字。`;
160
+ 请只输出 JSON,不要其他文字。`;
85
161
 
86
- const response = await aiClient.sendAndWait(prompt);
87
- await aiClient.stop();
162
+ const response = await AIClient.sendAndWait(prompt, config.ai.model);
88
163
 
89
164
  try {
90
- const groups = AIClient.parseJSON(response);
91
- logger.succeedSpinner(`AI 分析完成,共分為 ${groups.length} 個群組`);
92
- return groups;
165
+ return AIClient.parseJSON(response);
93
166
  } catch (error) {
94
- logger.failSpinner('AI 分析失敗');
95
- throw new Error(`無法解析 AI 回應: ${error.message}`);
167
+ console.error('❌ 无法解析 AI 回应:', error.message);
168
+ console.log('原始回应:', response);
169
+ return null;
96
170
  }
97
171
  }
98
172
 
99
173
  /**
100
- * 為特定群組生成 commit message
174
+ * 为特定群组生成 commit message
101
175
  */
102
- async function generateCommitMessage(group, files, config, logger) {
176
+ async function generateCommitMessage(group, files, config) {
103
177
  const filesList = files
104
- .map(file => {
105
- const diff = GitOperations.getFileDiff(file.filePath, file.isNew);
106
- return `檔案: ${file.filePath}\n${diff}`;
178
+ .map((file) => {
179
+ const diff = getFileDiff(file.filePath, file.isNew);
180
+ return `文件: ${file.filePath}\n${diff}`;
107
181
  })
108
182
  .join('\n\n---\n\n');
109
183
 
110
- const aiClient = new AIClient(config);
184
+ const prompt = `请根据以下资讯生成一则 commit message:
111
185
 
112
- const prompt = `請根據以下資訊生成一則 commit message:
186
+ 群组名称: ${group.group_name}
187
+ Commit 类型: ${group.commit_type}
188
+ Commit 范围: ${group.commit_scope || '未指定'}
189
+ 说明: ${group.description}
113
190
 
114
- 群組名稱: ${group.group_name}
115
- Commit 類型: ${group.commit_type}
116
- Commit 範圍: ${group.commit_scope || '未指定'}
117
- 說明: ${group.description}
118
-
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 字內,使用繁體中文
127
- - 如果變更複雜,可以加上 body(用空行分隔),body 使用 bullet points
128
- - 只輸出 commit message 本身,不要其他說明
129
- - 不要包含 markdown code block 標記(不要 \`\`\`)
130
- - 不要加上任何引導語句
198
+ - subject 限制在 50 字内,使用繁体中文
199
+ - 如果变更复杂,可以加上 body(用空行分隔),body 使用 bullet points
200
+ - 只输出 commit message 本身,不要其他说明
201
+ - 不要包含 markdown code block 标记(不要 \`\`\`)
202
+ - 不要加上任何引导语句
131
203
 
132
- 輸出格式範例:
204
+ 输出格式范例:
133
205
  feat(auth): 新增使用者登入功能
134
206
 
135
- - 實作登入 API endpoint
136
- - 新增登入頁面 UI
137
- - 整合 JWT 認證機制`;
207
+ - 实作登入 API endpoint
208
+ - 新增登入页面 UI
209
+ - 整合 JWT 认证机制`;
210
+
211
+ const response = await AIClient.sendAndWait(prompt, config.ai.model);
138
212
 
139
- const response = await aiClient.sendAndWait(prompt);
140
- await aiClient.stop();
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 AIClient.cleanResponse(response);
220
+ return commitMessage;
143
221
  }
144
222
 
145
223
  /**
146
- * 執行分組提交
224
+ * 执行分组提交
147
225
  */
148
- async function commitGroup(group, files, config, logger) {
226
+ async function commitGroup(group, files, config) {
149
227
  try {
150
- console.log(chalk.cyan(`\n📦 處理群組: ${group.group_name}`));
151
- console.log(` 類型: ${group.commit_type}${group.commit_scope ? `(${group.commit_scope})` : ''}`);
152
- console.log(` 檔案數量: ${files.length}`);
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
- // Reset 所有已 staged 的檔案
155
- GitOperations.resetStaged();
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
- GitOperations.addFile(file.filePath);
163
- addedFiles.push(file.filePath);
164
- } catch (error) {
165
- logger.warn(`無法 add 檔案 ${file.filePath}: ${error.message}`);
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, logger);
254
+ const commitMessage = await generateCommitMessage(group, files, config);
180
255
 
181
256
  if (!commitMessage) {
182
- logger.warn('無法生成 commit message,跳過此群組');
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(chalk.cyan('\n 📝 Commit Message:'));
194
- logger.code(commitMessage.split('\n').map(line => ` ${line}`).join('\n'));
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
- // 執行 commit
197
- await GitOperations.commit(commitMessage);
198
- logger.success('Commit 完成!');
268
+ // 执行 commit
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
- logger.error(`Commit 失敗: ${error.message}`);
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(options) {
211
- const logger = new Logger(options.verbose);
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
- }
301
+ // 载入配置
302
+ const config = await loadCommitConfig();
221
303
 
222
- // 載入配置
223
- const config = await loadConfig(options);
304
+ logger.header('智能分析所有变更并自动提交');
224
305
 
225
306
  if (config.output.verbose) {
226
- logger.debug('配置已載入');
227
- logger.debug(`AI Model: ${config.ai.model}`);
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.startSpinner('掃描變更中...');
232
- const changes = GitOperations.getAllChanges();
233
- logger.succeedSpinner(`找到 ${changes.length} 個變更的檔案`);
314
+ // 1. 获取所有变更
315
+ logger.step('扫描变更中...');
316
+ const changes = getAllChanges();
234
317
 
235
318
  if (changes.length === 0) {
236
- logger.info('沒有需要提交的變更');
319
+ logger.info('没有需要提交的变更');
237
320
  process.exit(0);
238
321
  }
239
322
 
240
- console.log(chalk.cyan('\n📊 變更的檔案:'));
241
- console.log(formatFileList(changes));
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, logger);
330
+ // 2. 使用 AI 分析并分组
331
+ const groups = await analyzeAndGroupChanges(changes, config);
245
332
 
246
333
  if (!groups || groups.length === 0) {
247
- logger.error('AI 分析失敗或沒有產生分組');
334
+ logger.error('AI 分析失败或没有产生分组');
248
335
  process.exit(1);
249
336
  }
250
337
 
251
- console.log(chalk.cyan('\n✅ 分組結果:'));
338
+ logger.success(`AI 分析完成,共分为 ${groups.length} 个群组:\n`);
252
339
  groups.forEach((group, index) => {
253
- console.log(` 群組 ${index + 1}: ${group.group_name} (${group.commit_type})`);
254
- console.log(` └─ 包含 ${group.file_indices.length} 個檔案`);
340
+ console.log(` 群组 ${index + 1}: ${group.group_name} (${group.commit_type})`);
341
+ console.log(` └─ 包含 ${group.file_indices.length} 个文件`);
255
342
  });
256
343
 
257
- // 依序提交每個群組
258
- logger.header('開始執行提交');
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, validFiles, config, logger);
354
+ const success = await commitGroup(group, groupFiles, config);
292
355
  if (success) {
293
356
  successCount++;
294
357
  }
295
358
  }
296
359
 
297
- // 顯示摘要
298
- logger.header(`完成!成功提交 ${successCount}/${groups.length} 個群組`);
360
+ // 4. 显示摘要
361
+ logger.separator('=', 60);
362
+ logger.success(`完成!成功提交 ${successCount}/${groups.length} 个群组`);
363
+ logger.separator('=', 60);
364
+
365
+ // 显示最近的几个 commits
366
+ console.log('\n📋 最近的 commits:');
367
+ execSync(`git log -${successCount} --oneline`, { stdio: 'inherit' });
299
368
 
300
- // 顯示最近的 commits
301
- console.log(chalk.cyan('\n📋 最近的 commits:'));
369
+ // Reset 任何剩余的 staged 文件
302
370
  try {
303
- const recentCommits = GitOperations.getRecentCommits(successCount);
304
- if (recentCommits) {
305
- console.log(recentCommits);
306
- }
307
- } catch (error) {
308
- // 忽略顯示 commit 的錯誤
371
+ execSync('git reset HEAD -- .', { stdio: 'ignore' });
372
+ } catch (e) {
373
+ // 忽略
309
374
  }
310
-
311
- // Reset 任何剩餘的 staged 檔案
312
- GitOperations.resetStaged();
313
-
314
375
  } catch (error) {
315
376
  handleError(error);
316
377
  process.exit(1);