ai-git-tools 2.0.78 → 2.0.80
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/README.md +2 -0
- package/bin/cli.js +39 -61
- package/package.json +1 -1
- package/src/ai/prompts/commit-message.js +42 -0
- package/src/ai/prompts/pr-content.js +151 -0
- package/src/commands/commit-all.js +23 -12
- package/src/commands/commit.js +92 -103
- package/src/commands/pr.js +22 -31
- package/src/commands/usage.js +557 -0
- package/src/core/ai-client.js +33 -6
- package/src/core/config-loader.js +151 -113
- package/src/core/git-operations.js +122 -39
- package/src/pr-modules/ai/code-analyzer.js +19 -141
- package/src/pr-modules/ai/label-analyzer.js +1 -1
- package/src/pr-modules/core/github-api.js +56 -5
- package/src/pr-modules/core/workflow.js +25 -5
- package/src/pr-modules/reviewers/reviewer-selector.js +2 -2
- package/src/pr-modules/ui/interactive-select.js +1 -1
- package/src/utils/cli-helpers.js +51 -0
- package/src/utils/constants.js +49 -0
- package/src/utils/helpers.js +74 -3
- package/src/utils/logger.js +13 -11
- package/src/{pr-modules/utils/constants.js → utils/project-skills.js} +26 -49
- package/src/pr-modules/core/config-loader.js +0 -132
- package/src/pr-modules/core/git-operations.js +0 -248
- package/src/pr-modules/ui/logger.js +0 -40
- package/src/pr-modules/utils/helpers.js +0 -75
package/README.md
CHANGED
package/bin/cli.js
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* AI Git Tools CLI
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* AI-powered Git automation for commit messages and PR generation
|
|
7
|
-
* 完全重寫版本基於 scripts/ 原始實現
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
import { Command } from 'commander';
|
|
@@ -15,6 +14,8 @@ import { commitCommand } from '../src/commands/commit.js';
|
|
|
15
14
|
import { commitAllCommand } from '../src/commands/commit-all.js';
|
|
16
15
|
import { prCommand } from '../src/commands/pr.js';
|
|
17
16
|
import { initCommand } from '../src/commands/init.js';
|
|
17
|
+
import { usageCommand } from '../src/commands/usage.js';
|
|
18
|
+
import { registerCommand } from '../src/utils/cli-helpers.js';
|
|
18
19
|
|
|
19
20
|
// 讀取 package.json 獲取版本號
|
|
20
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -31,70 +32,47 @@ program
|
|
|
31
32
|
.version(packageJson.version);
|
|
32
33
|
|
|
33
34
|
// Init 命令
|
|
34
|
-
program
|
|
35
|
-
.command('init')
|
|
36
|
-
.description('初始化配置檔案 (.ai-git-config.mjs)')
|
|
37
|
-
.action(async (options) => {
|
|
38
|
-
try {
|
|
39
|
-
await initCommand(options);
|
|
40
|
-
process.exit(0);
|
|
41
|
-
} catch (error) {
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
});
|
|
35
|
+
registerCommand(program, 'init', '初始化配置檔案 (.ai-git-config.mjs)', [], initCommand);
|
|
45
36
|
|
|
46
37
|
// Commit 命令
|
|
47
|
-
program
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
.option('--max-retries <number>', '最大重試次數')
|
|
54
|
-
.action(async (options) => {
|
|
55
|
-
try {
|
|
56
|
-
await commitCommand(options);
|
|
57
|
-
process.exit(0);
|
|
58
|
-
} catch (error) {
|
|
59
|
-
process.exit(1);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
38
|
+
registerCommand(program, 'commit', 'AI 自動生成 commit message 並提交', [
|
|
39
|
+
{ flags: '--model <model>', description: '指定 AI 模型' },
|
|
40
|
+
{ flags: '-v, --verbose', description: '顯示詳細輸出' },
|
|
41
|
+
{ flags: '--max-diff <number>', description: '最大 diff 長度' },
|
|
42
|
+
{ flags: '--max-retries <number>', description: '最大重試次數' },
|
|
43
|
+
], commitCommand);
|
|
62
44
|
|
|
63
45
|
// Commit All 命令
|
|
64
|
-
program
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
.option('--max-retries <number>', '最大重試次數')
|
|
71
|
-
.action(async (options) => {
|
|
72
|
-
try {
|
|
73
|
-
await commitAllCommand(options);
|
|
74
|
-
process.exit(0);
|
|
75
|
-
} catch (error) {
|
|
76
|
-
process.exit(1);
|
|
77
|
-
}
|
|
78
|
-
});
|
|
46
|
+
registerCommand(program, 'commit-all', '智慧分析所有變更並自動分組提交', [
|
|
47
|
+
{ flags: '--model <model>', description: '指定 AI 模型' },
|
|
48
|
+
{ flags: '-v, --verbose', description: '顯示詳細輸出' },
|
|
49
|
+
{ flags: '--max-diff <number>', description: '最大 diff 長度' },
|
|
50
|
+
{ flags: '--max-retries <number>', description: '最大重試次數' },
|
|
51
|
+
], commitAllCommand);
|
|
79
52
|
|
|
80
53
|
// PR 命令
|
|
81
|
-
program
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
54
|
+
registerCommand(program, 'pr', 'AI 自動生成 PR 並創建 Pull Request', [
|
|
55
|
+
{ flags: '--base <branch>', description: '指定目標分支' },
|
|
56
|
+
{ flags: '--model <model>', description: '指定 AI 模型' },
|
|
57
|
+
{ flags: '--preview', description: '僅預覽 PR 內容,不實際創建' },
|
|
58
|
+
{ flags: '--no-confirm', description: '跳過確認直接創建' },
|
|
59
|
+
{ flags: '--auto-labels', description: '自動添加 Labels (預設啟用)' },
|
|
60
|
+
{ flags: '--include-impact', description: '在 PR 中包含影響範圍分析和注意事項 (預設關閉)' },
|
|
61
|
+
{ flags: '--force-new', description: '強制創建新 PR,不更新現有 PR' },
|
|
62
|
+
], prCommand);
|
|
63
|
+
|
|
64
|
+
// Usage 命令
|
|
65
|
+
registerCommand(program, 'usage', '查看組織 GitHub Copilot 使用狀態與用量', [
|
|
66
|
+
{ flags: '--org <org>', description: '指定組織名稱(預設自動從 git remote 偵測)' },
|
|
67
|
+
{ flags: '--from <date>', description: '開始日期,格式 YYYY-MM-DD(預設:本月第一天)' },
|
|
68
|
+
{ flags: '--to <date>', description: '結束日期,格式 YYYY-MM-DD(預設:今天)' },
|
|
69
|
+
{ flags: '--top <n>', description: '只顯示前 N 名用戶' },
|
|
70
|
+
{ flags: '--sort <by>', description: '排序方式:credits(預設)| amount | name | activity' },
|
|
71
|
+
{ flags: '--team <slug>', description: '只顯示指定團隊的成員' },
|
|
72
|
+
{ flags: '--inactive', description: '同時顯示非活躍用戶' },
|
|
73
|
+
{ flags: '--breakdown', description: '顯示每日使用量明細' },
|
|
74
|
+
{ flags: '--export <file>', description: '匯出為 CSV 檔案(例如 usage.csv)' },
|
|
75
|
+
{ flags: '--json', description: '以 JSON 格式輸出完整資料' },
|
|
76
|
+
], usageCommand);
|
|
99
77
|
|
|
100
78
|
program.parse();
|
package/package.json
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commit Message Prompt 生成
|
|
3
|
+
* 集中管理 commit message 的 AI prompt 與後處理
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getProjectTypePrompt } from '../../utils/helpers.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 生成 commit message prompt
|
|
10
|
+
* @param {string} diff - git diff 內容
|
|
11
|
+
* @param {Object} config - 配置物件
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function generateCommitMessagePrompt(diff, config = {}) {
|
|
15
|
+
const projectPrompt = getProjectTypePrompt();
|
|
16
|
+
|
|
17
|
+
return `${projectPrompt}
|
|
18
|
+
|
|
19
|
+
請根據以下 git diff 產生一則 commit message。
|
|
20
|
+
|
|
21
|
+
**Commit Message 規則**:
|
|
22
|
+
1. 使用 Conventional Commits 格式:type(scope): subject
|
|
23
|
+
2. type 必須是:feat/fix/docs/style/refactor/test/chore/perf 其中之一
|
|
24
|
+
3. scope: 影響範圍(如 member、report、auth、api、ui、config)
|
|
25
|
+
4. subject 限制在 50 字內,使用繁體中文
|
|
26
|
+
5. 如果變更複雜,加上 body 說明(使用 bullet points)
|
|
27
|
+
|
|
28
|
+
**重要**:
|
|
29
|
+
- 直接輸出 commit message 純文字,不要使用 markdown 程式碼區塊(\`\`\`)
|
|
30
|
+
- 不要加上任何前綴說明或後綴文字
|
|
31
|
+
- 第一行是標題,如有需要可加上空行後的詳細說明
|
|
32
|
+
|
|
33
|
+
**範例格式**:
|
|
34
|
+
feat(member): 新增會員管理頁面
|
|
35
|
+
|
|
36
|
+
- 實作會員列表查詢功能
|
|
37
|
+
- 新增會員資料編輯表單
|
|
38
|
+
- 整合 Zustand 狀態管理
|
|
39
|
+
|
|
40
|
+
git diff:
|
|
41
|
+
${diff}`;
|
|
42
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR Content Prompt 生成
|
|
3
|
+
* 集中管理 PR 標題、描述與影響分析的 AI prompt
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { CONSTANTS } from '../../utils/constants.js';
|
|
7
|
+
import { getSkillsSummaryForPrompt, PROJECT_SKILLS_CONTEXT } from '../../utils/project-skills.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 生成 PR 內容 prompt
|
|
11
|
+
* @param {string} commits - commit 訊息
|
|
12
|
+
* @param {string} diff - 程式碼變更 diff
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export function generatePRContentPrompt(commits, diff) {
|
|
16
|
+
return `你是一個專業的前端工程師,熟悉 Next.js、React 效能優化和團隊開發規範。
|
|
17
|
+
請根據以下 commit 訊息和程式碼變更,直接輸出一個清晰的 Pull Request 標題和描述。
|
|
18
|
+
|
|
19
|
+
**重要原則**:
|
|
20
|
+
- 只根據實際的 commit 訊息和 diff 內容描述變更,不要臆測或誇大
|
|
21
|
+
- 如果 commit 中沒有提到「新增指令 / 新增 API / 新增組件」,請不要用這些詞
|
|
22
|
+
- 文件(如 .github/copilot-instructions.md、prompt 檔案、README)應描述為「新增/更新文件」而非「新增指令」
|
|
23
|
+
- 如果 OpenSpec prompt 被刪除並改為 OpsX prompt,請描述為「以 OpsX 取代 OpenSpec」,不要說「新增 OpenSpec prompt」
|
|
24
|
+
- package.json 若只有版本號變更,請描述為「更新版本號」,不要說「更新相依」
|
|
25
|
+
- 重構相關的改動請優先使用 refactor 類型
|
|
26
|
+
|
|
27
|
+
**輸出格式**(不要加任何引導語,直接輸出以下內容):
|
|
28
|
+
|
|
29
|
+
# [type]: [PR 標題]
|
|
30
|
+
|
|
31
|
+
> type 必須是以下之一:feat / fix / refactor / style / docs / test / chore / perf
|
|
32
|
+
> **重要**:如果有新增任何功能、新增檔案、新增 API、新增組件,優先使用 **feat**
|
|
33
|
+
|
|
34
|
+
## 📝 變更摘要
|
|
35
|
+
[簡述這個 PR 的主要目的和影響範圍,2-3 句話]
|
|
36
|
+
|
|
37
|
+
## 🎯 主要變更
|
|
38
|
+
- [變更項目 1]
|
|
39
|
+
- [變更項目 2]
|
|
40
|
+
- [變更項目 3]
|
|
41
|
+
|
|
42
|
+
## 🔀 變更類型
|
|
43
|
+
- [ ] ✨ 新功能 (feat)
|
|
44
|
+
- [ ] 🐛 Bug 修復 (fix)
|
|
45
|
+
- [ ] ♻️ 重構 (refactor)
|
|
46
|
+
- [ ] 💄 樣式調整 (style)
|
|
47
|
+
- [ ] 📝 文件更新 (docs)
|
|
48
|
+
- [ ] ⚡ 效能改進 (perf)
|
|
49
|
+
- [ ] 🔧 其他 (chore)
|
|
50
|
+
|
|
51
|
+
> 根據 diff 和 commit 自動勾選(可複選),[ ] 改為 [x];有新增檔案或功能必勾 ✨ feat
|
|
52
|
+
|
|
53
|
+
## 🧪 測試方法
|
|
54
|
+
1. [具體的測試步驟 1]
|
|
55
|
+
2. [具體的測試步驟 2]
|
|
56
|
+
3. [具體的測試步驟 3]
|
|
57
|
+
|
|
58
|
+
## 💥 Breaking Changes
|
|
59
|
+
[如果有破壞性變更請詳細說明,沒有則填寫「無」]
|
|
60
|
+
|
|
61
|
+
## 📌 注意事項
|
|
62
|
+
[需要特別注意的事項]
|
|
63
|
+
|
|
64
|
+
## 📸 截圖
|
|
65
|
+
[如果是 UI 變更,提醒需要截圖]
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## ⚠️ 風險與注意事項
|
|
70
|
+
**Risk Level**: \`LOW\` / \`MEDIUM\` / \`HIGH\`
|
|
71
|
+
|
|
72
|
+
[說明潛在風險、破壞性變更(breaking changes)、需要特別小心的地方;沒有則填「無」]
|
|
73
|
+
|
|
74
|
+
## 👀 Reviewer 重點
|
|
75
|
+
- [請 reviewer 特別關注的邏輯或設計決策 1]
|
|
76
|
+
- [請 reviewer 特別關注的邏輯或設計決策 2]
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
**規則**:
|
|
81
|
+
- 直接輸出 # [type]: [標題],繁體中文(台灣正體)
|
|
82
|
+
- type 必須符合 Conventional Commits;以 commit 內容為準,新增檔案/功能優先 feat,重構優先 refactor
|
|
83
|
+
- 變更類型只勾選實際出現的類型,沒有 fix 類 commit 就不要勾 Bug 修復
|
|
84
|
+
- Risk Level:HIGH=核心流程,MEDIUM=影響現有功能,LOW=新增或純重構
|
|
85
|
+
- Reviewer 重點列 1-3 個值得仔細看的地方
|
|
86
|
+
- 描述必須和 commit 訊息一致,禁止虛構功能或誇大影響範圍
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
**Commit 訊息**:
|
|
91
|
+
${commits}
|
|
92
|
+
|
|
93
|
+
**程式碼變更**:
|
|
94
|
+
${diff}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 生成影響分析 prompt
|
|
99
|
+
* @param {string[]} changedFiles - 變更檔案列表
|
|
100
|
+
* @param {string} diff - 程式碼變更 diff
|
|
101
|
+
* @param {string} commits - commit 訊息
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
export function generateImpactAnalysisPrompt(changedFiles, diff, commits) {
|
|
105
|
+
const skillsSummary = getSkillsSummaryForPrompt(PROJECT_SKILLS_CONTEXT);
|
|
106
|
+
|
|
107
|
+
return `你是一個資深的程式碼審查專家,精通 React/Next.js 效能優化與前端架構設計。
|
|
108
|
+
請分析以下程式碼變更,提供專業的影響範圍分析與規範合規檢查。
|
|
109
|
+
|
|
110
|
+
${skillsSummary}
|
|
111
|
+
|
|
112
|
+
**變更檔案列表**:
|
|
113
|
+
${changedFiles.slice(0, CONSTANTS.MAX_FILES_IN_PROMPT).join('\n')}
|
|
114
|
+
${
|
|
115
|
+
changedFiles.length > CONSTANTS.MAX_FILES_IN_PROMPT
|
|
116
|
+
? `... 還有 ${changedFiles.length - CONSTANTS.MAX_FILES_IN_PROMPT} 個檔案`
|
|
117
|
+
: ''
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
**Commit 訊息**:
|
|
121
|
+
${commits.split('\n').slice(0, CONSTANTS.MAX_COMMITS_IN_PROMPT).join('\n')}
|
|
122
|
+
|
|
123
|
+
**程式碼變更內容**:
|
|
124
|
+
\`\`\`diff
|
|
125
|
+
${diff.substring(0, CONSTANTS.MAX_DIFF_LENGTH)}
|
|
126
|
+
${diff.length > CONSTANTS.MAX_DIFF_LENGTH ? '\n... (內容過長已截斷)' : ''}
|
|
127
|
+
\`\`\`
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
請以 JSON 格式輸出分析結果(不要加任何其他文字,只輸出 JSON):
|
|
132
|
+
|
|
133
|
+
\`\`\`json
|
|
134
|
+
{
|
|
135
|
+
"blastRadius": {
|
|
136
|
+
"modules": ["影響的模組1", "影響的模組2"],
|
|
137
|
+
"impacts": ["影響層面1", "影響層面2"],
|
|
138
|
+
"riskLevel": "低|中|高",
|
|
139
|
+
"riskReasons": ["風險原因1", "風險原因2"],
|
|
140
|
+
"externalBehaviors": ["對外行為變更說明"]
|
|
141
|
+
},
|
|
142
|
+
"warnings": [
|
|
143
|
+
{
|
|
144
|
+
"level": "⚠️|ℹ️",
|
|
145
|
+
"message": "問題描述",
|
|
146
|
+
"suggestion": "改善建議"
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
\`\`\``;
|
|
151
|
+
}
|
|
@@ -9,7 +9,7 @@ import { readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
|
9
9
|
import { loadCommitConfig } from '../core/config-loader.js';
|
|
10
10
|
import { AIClient } from '../core/ai-client.js';
|
|
11
11
|
import { Logger } from '../utils/logger.js';
|
|
12
|
-
import { handleError } from '../utils/helpers.js';
|
|
12
|
+
import { handleError, isCopilotSubscriptionError } from '../utils/helpers.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* 獲取檔案的變更內容
|
|
@@ -54,7 +54,7 @@ function getAllChanges() {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
const changes = [];
|
|
57
|
-
const lines = status.split('\n').filter(
|
|
57
|
+
const lines = status.split('\n').filter(line => line.trim());
|
|
58
58
|
|
|
59
59
|
for (const line of lines) {
|
|
60
60
|
const statusCode = line.substring(0, 2);
|
|
@@ -184,8 +184,19 @@ ${changeSummary}
|
|
|
184
184
|
try {
|
|
185
185
|
return AIClient.parseJSON(response);
|
|
186
186
|
} catch (error) {
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
// 區分不同類型的錯誤
|
|
188
|
+
if (isCopilotSubscriptionError(error)) {
|
|
189
|
+
console.error('\n🔑 看起來是 GitHub Copilot 授權問題');
|
|
190
|
+
console.error('\n解決方案:');
|
|
191
|
+
console.error(' 1. 確認你的 GitHub 帳號已訂閱 GitHub Copilot');
|
|
192
|
+
console.error(' 2. 驗證 VS Code 中使用的 GitHub 帳號是否有 Copilot 存取權限');
|
|
193
|
+
console.error(' 3. 嘗試重新登入:');
|
|
194
|
+
console.error(' gh auth logout');
|
|
195
|
+
console.error(' gh auth login');
|
|
196
|
+
} else {
|
|
197
|
+
console.error('❌ 無法解析 AI 回應:', error.message);
|
|
198
|
+
console.log('原始回應:', response);
|
|
199
|
+
}
|
|
189
200
|
return null;
|
|
190
201
|
}
|
|
191
202
|
}
|
|
@@ -197,7 +208,7 @@ async function generateCommitMessage(group, files, config) {
|
|
|
197
208
|
// 每個檔案最多 2000 字元,避免單一群組內大量 diff 超出限制
|
|
198
209
|
const MAX_DIFF_PER_FILE = 2000;
|
|
199
210
|
const filesList = files
|
|
200
|
-
.map(
|
|
211
|
+
.map(file => {
|
|
201
212
|
const diff = getFileDiff(file.filePath, file.isNew, file.isDeleted);
|
|
202
213
|
const truncatedDiff =
|
|
203
214
|
diff.length > MAX_DIFF_PER_FILE
|
|
@@ -295,7 +306,7 @@ async function commitGroup(group, files, config) {
|
|
|
295
306
|
|
|
296
307
|
console.log(`\n 📝 Commit Message:`);
|
|
297
308
|
console.log(` ${'─'.repeat(50)}`);
|
|
298
|
-
commitMessage.split('\n').forEach(
|
|
309
|
+
commitMessage.split('\n').forEach(line => {
|
|
299
310
|
console.log(` ${line}`);
|
|
300
311
|
});
|
|
301
312
|
console.log(` ${'─'.repeat(50)}`);
|
|
@@ -386,8 +397,8 @@ export async function commitAllCommand() {
|
|
|
386
397
|
|
|
387
398
|
// 驗證所有檔案都被包含在分組中
|
|
388
399
|
const groupedIndices = new Set();
|
|
389
|
-
groups.forEach(
|
|
390
|
-
group.file_indices.forEach(
|
|
400
|
+
groups.forEach(group => {
|
|
401
|
+
group.file_indices.forEach(index => {
|
|
391
402
|
groupedIndices.add(index);
|
|
392
403
|
});
|
|
393
404
|
});
|
|
@@ -402,7 +413,7 @@ export async function commitAllCommand() {
|
|
|
402
413
|
// 如果有檔案未被分組,創建一個 "其他變更" 群組
|
|
403
414
|
if (ungroupedIndices.length > 0) {
|
|
404
415
|
console.log(`\n⚠️ 發現 ${ungroupedIndices.length} 個未分組的檔案,將自動歸類:`);
|
|
405
|
-
ungroupedIndices.forEach(
|
|
416
|
+
ungroupedIndices.forEach(index => {
|
|
406
417
|
console.log(` - ${changes[index].filePath}`);
|
|
407
418
|
});
|
|
408
419
|
|
|
@@ -420,7 +431,7 @@ export async function commitAllCommand() {
|
|
|
420
431
|
console.log(` 群組 ${index + 1}: ${group.group_name} (${group.commit_type})`);
|
|
421
432
|
console.log(` └─ 包含 ${group.file_indices.length} 個檔案`);
|
|
422
433
|
if (config.output.verbose) {
|
|
423
|
-
group.file_indices.forEach(
|
|
434
|
+
group.file_indices.forEach(fileIndex => {
|
|
424
435
|
console.log(` - [${fileIndex}] ${changes[fileIndex].filePath}`);
|
|
425
436
|
});
|
|
426
437
|
}
|
|
@@ -434,10 +445,10 @@ export async function commitAllCommand() {
|
|
|
434
445
|
let successCount = 0;
|
|
435
446
|
for (let i = 0; i < groups.length; i++) {
|
|
436
447
|
const group = groups[i];
|
|
437
|
-
const groupFiles = group.file_indices.map(
|
|
448
|
+
const groupFiles = group.file_indices.map(index => changes[index]);
|
|
438
449
|
|
|
439
450
|
// 驗證檔案索引是否有效
|
|
440
|
-
const invalidIndices = group.file_indices.filter(
|
|
451
|
+
const invalidIndices = group.file_indices.filter(idx => idx >= changes.length);
|
|
441
452
|
if (invalidIndices.length > 0) {
|
|
442
453
|
console.error(`\n❌ 群組 ${i + 1} 包含無效的檔案索引:`, invalidIndices);
|
|
443
454
|
console.log(` 跳過此群組: ${group.group_name}`);
|
package/src/commands/commit.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Commit 命令
|
|
3
|
-
* 基於 scripts/ai-auto-commit.mjs
|
|
4
3
|
* 自動產生 commit message 並執行 commit
|
|
5
4
|
*/
|
|
6
5
|
|
|
@@ -9,129 +8,119 @@ import { loadCommitConfig } from '../core/config-loader.js';
|
|
|
9
8
|
import { GitOperations } from '../core/git-operations.js';
|
|
10
9
|
import { AIClient } from '../core/ai-client.js';
|
|
11
10
|
import { Logger } from '../utils/logger.js';
|
|
12
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
cleanCommitMessage,
|
|
13
|
+
validateCommitMessage,
|
|
14
|
+
isCopilotSubscriptionError,
|
|
15
|
+
} from '../utils/helpers.js';
|
|
16
|
+
import { generateCommitMessagePrompt } from '../ai/prompts/commit-message.js';
|
|
13
17
|
|
|
14
18
|
export async function commitCommand() {
|
|
15
19
|
const logger = new Logger();
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const config = await loadCommitConfig();
|
|
21
|
+
// 載入配置
|
|
22
|
+
const config = await loadCommitConfig();
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
if (config.output.verbose) {
|
|
25
|
+
console.log('📋 使用配置:');
|
|
26
|
+
console.log(` AI Model: ${config.ai.model}`);
|
|
27
|
+
console.log(` Max Diff Length: ${config.ai.maxDiffLength}`);
|
|
28
|
+
console.log('');
|
|
29
|
+
}
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
// 檢查是否有 staged 變更
|
|
32
|
+
const diff = GitOperations.getStagedDiff();
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
if (!diff.trim()) {
|
|
35
|
+
logger.error('沒有 staged 的變更');
|
|
36
|
+
console.log('💡 請先使用 git add 來 stage 你的變更');
|
|
37
|
+
throw new Error('沒有 staged 的變更');
|
|
38
|
+
}
|
|
36
39
|
|
|
37
|
-
|
|
40
|
+
logger.step('正在分析變更內容...\n');
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
// 截斷過長的 diff
|
|
43
|
+
const truncatedDiff =
|
|
44
|
+
diff.length > config.ai.maxDiffLength
|
|
45
|
+
? diff.substring(0, config.ai.maxDiffLength) + '\n\n... [diff 過長已截斷]'
|
|
46
|
+
: diff;
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
if (config.output.verbose && diff.length > config.ai.maxDiffLength) {
|
|
49
|
+
logger.warning(`Diff 已從 ${diff.length} 字元截斷至 ${config.ai.maxDiffLength} 字元\n`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 使用 AI 產生 commit message
|
|
53
|
+
let commitMessage = '';
|
|
54
|
+
let lastError = null;
|
|
48
55
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
feat(member): 新增會員管理頁面
|
|
77
|
-
|
|
78
|
-
- 實作會員列表查詢功能
|
|
79
|
-
- 新增會員資料編輯表單
|
|
80
|
-
- 整合 Zustand 狀態管理
|
|
81
|
-
|
|
82
|
-
git diff:
|
|
83
|
-
${truncatedDiff}`;
|
|
84
|
-
|
|
85
|
-
const response = await AIClient.sendAndWait(prompt, config.ai.model);
|
|
86
|
-
commitMessage = cleanCommitMessage(response);
|
|
87
|
-
|
|
88
|
-
// 驗證 commit message
|
|
89
|
-
const validation = validateCommitMessage(commitMessage);
|
|
90
|
-
if (!validation.valid) {
|
|
91
|
-
throw new Error(`無效的 commit message: ${validation.reason}`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// 成功產生,跳出重試迴圈
|
|
95
|
-
break;
|
|
96
|
-
} catch (error) {
|
|
97
|
-
lastError = error;
|
|
98
|
-
if (config.output.verbose) {
|
|
99
|
-
logger.warning(`嘗試 ${attempt} 失敗: ${error.message}\n`);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (attempt < config.ai.maxRetries) {
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
56
|
+
for (let attempt = 1; attempt <= config.ai.maxRetries; attempt++) {
|
|
57
|
+
try {
|
|
58
|
+
if (config.output.verbose && attempt > 1) {
|
|
59
|
+
console.log(`🔄 重試第 ${attempt}/${config.ai.maxRetries} 次...\n`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const prompt = generateCommitMessagePrompt(truncatedDiff, config);
|
|
63
|
+
|
|
64
|
+
const response = await AIClient.sendAndWait(prompt, config.ai.model);
|
|
65
|
+
commitMessage = cleanCommitMessage(response);
|
|
66
|
+
|
|
67
|
+
// 驗證 commit message
|
|
68
|
+
const validation = validateCommitMessage(commitMessage);
|
|
69
|
+
if (!validation.valid) {
|
|
70
|
+
throw new Error(`無效的 commit message: ${validation.reason}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 成功產生,跳出重試迴圈
|
|
74
|
+
break;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
lastError = error;
|
|
77
|
+
if (config.output.verbose) {
|
|
78
|
+
logger.warning(`嘗試 ${attempt} 失敗: ${error.message}\n`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (attempt < config.ai.maxRetries) {
|
|
82
|
+
continue;
|
|
105
83
|
}
|
|
106
84
|
}
|
|
85
|
+
}
|
|
107
86
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
87
|
+
// 所有重試都失敗
|
|
88
|
+
if (!commitMessage) {
|
|
89
|
+
logger.error('無法產生有效的 commit message');
|
|
90
|
+
|
|
91
|
+
// 區分不同類型的錯誤
|
|
92
|
+
if (isCopilotSubscriptionError(lastError)) {
|
|
93
|
+
console.log('\n🔑 看起來是 GitHub Copilot 授權問題\n');
|
|
94
|
+
console.log('解決方案:');
|
|
95
|
+
console.log(' 1. 確認你的 GitHub 帳號已訂閱 GitHub Copilot');
|
|
96
|
+
console.log(' 2. 驗證 VS Code 中使用的 GitHub 帳號是否有 Copilot 訪問權限');
|
|
97
|
+
console.log(' 3. 嘗試重新登入:');
|
|
98
|
+
console.log(' gh auth logout');
|
|
99
|
+
console.log(' gh auth login');
|
|
100
|
+
console.log(' 4. 若是公司帳號,確保使用公司的 GitHub 帳號登入');
|
|
101
|
+
} else {
|
|
102
|
+
if (config.output.verbose && lastError) {
|
|
112
103
|
console.log(` 最後錯誤: ${lastError.message}`);
|
|
113
104
|
}
|
|
114
105
|
console.log('\n💡 建議:');
|
|
115
106
|
console.log(' 1. 檢查網路連線');
|
|
116
107
|
console.log(' 2. 嘗試更換 AI 模型(使用 --model 參數)');
|
|
117
108
|
console.log(' 3. 確認變更內容不會太複雜或太大');
|
|
118
|
-
throw new Error('無法產生有效的 commit message');
|
|
119
109
|
}
|
|
120
110
|
|
|
121
|
-
|
|
122
|
-
logger.separator('─', 60);
|
|
123
|
-
console.log(commitMessage);
|
|
124
|
-
logger.separator('─', 60);
|
|
125
|
-
|
|
126
|
-
// 執行 commit
|
|
127
|
-
logger.step('\n正在執行 commit...');
|
|
128
|
-
execSync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, {
|
|
129
|
-
stdio: 'inherit',
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
logger.success('Commit 完成!\n');
|
|
133
|
-
} catch (error) {
|
|
134
|
-
handleError(error);
|
|
135
|
-
throw error;
|
|
111
|
+
throw new Error('無法產生有效的 commit message');
|
|
136
112
|
}
|
|
113
|
+
|
|
114
|
+
logger.success('產生的 Commit Message:');
|
|
115
|
+
logger.separator('─', 60);
|
|
116
|
+
console.log(commitMessage);
|
|
117
|
+
logger.separator('─', 60);
|
|
118
|
+
|
|
119
|
+
// 執行 commit
|
|
120
|
+
logger.step('\n正在執行 commit...');
|
|
121
|
+
execSync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, {
|
|
122
|
+
stdio: 'inherit',
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
logger.success('Commit 完成!\n');
|
|
137
126
|
}
|