ai-git-tools 1.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.
- package/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +353 -0
- package/bin/cli.js +86 -0
- package/package.json +60 -0
- package/src/commands/commit-all.js +266 -0
- package/src/commands/commit.js +115 -0
- package/src/commands/init.js +163 -0
- package/src/commands/pr.js +305 -0
- package/src/commands/workflow.js +36 -0
- package/src/core/ai-client.js +115 -0
- package/src/core/config-loader.js +165 -0
- package/src/core/git-operations.js +263 -0
- package/src/core/github-api.js +139 -0
- package/src/index.js +16 -0
- package/src/utils/helpers.js +88 -0
- package/src/utils/logger.js +123 -0
- package/templates/.ai-git-config.template.js +38 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git 操作工具
|
|
3
|
+
*
|
|
4
|
+
* 封裝常用的 Git 命令操作
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import { readFileSync, existsSync } from 'fs';
|
|
9
|
+
|
|
10
|
+
export class GitOperations {
|
|
11
|
+
/**
|
|
12
|
+
* 執行 Git 命令
|
|
13
|
+
*/
|
|
14
|
+
static exec(command, options = {}) {
|
|
15
|
+
try {
|
|
16
|
+
return execSync(command, {
|
|
17
|
+
encoding: 'utf-8',
|
|
18
|
+
stdio: options.silent ? 'pipe' : 'inherit',
|
|
19
|
+
...options,
|
|
20
|
+
}).toString();
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (options.throwOnError !== false) {
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 檢查是否在 Git 倉庫中
|
|
31
|
+
*/
|
|
32
|
+
static isGitRepository() {
|
|
33
|
+
try {
|
|
34
|
+
GitOperations.exec('git rev-parse --git-dir', { silent: true });
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 獲取當前分支名稱
|
|
43
|
+
*/
|
|
44
|
+
static getCurrentBranch() {
|
|
45
|
+
return GitOperations.exec('git branch --show-current', { silent: true }).trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 獲取遠端 URL
|
|
50
|
+
*/
|
|
51
|
+
static getRemoteUrl(remoteName = 'origin') {
|
|
52
|
+
return GitOperations.exec(`git remote get-url ${remoteName}`, {
|
|
53
|
+
silent: true,
|
|
54
|
+
throwOnError: false,
|
|
55
|
+
}).trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 從遠端 URL 解析組織和倉庫名稱
|
|
60
|
+
*/
|
|
61
|
+
static parseRemoteUrl(url) {
|
|
62
|
+
const match = url.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
|
63
|
+
if (match) {
|
|
64
|
+
return {
|
|
65
|
+
org: match[1],
|
|
66
|
+
repo: match[2],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { org: null, repo: null };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 獲取 staged 變更的 diff
|
|
74
|
+
*/
|
|
75
|
+
static getStagedDiff() {
|
|
76
|
+
return GitOperations.exec('git diff --staged', { silent: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 獲取所有未提交的變更
|
|
81
|
+
*/
|
|
82
|
+
static getAllChanges() {
|
|
83
|
+
const status = GitOperations.exec('git status --porcelain', { silent: true });
|
|
84
|
+
|
|
85
|
+
if (!status.trim()) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const changes = [];
|
|
90
|
+
const lines = status.split('\n').filter(line => line.trim());
|
|
91
|
+
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
const statusCode = line.substring(0, 2);
|
|
94
|
+
const filePath = line.substring(3).trim();
|
|
95
|
+
|
|
96
|
+
// 跳過已刪除的檔案
|
|
97
|
+
if (statusCode.includes('D')) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 跳過不需要的檔案
|
|
102
|
+
if (
|
|
103
|
+
filePath.includes('node_modules/') ||
|
|
104
|
+
filePath.includes('.next/') ||
|
|
105
|
+
filePath.includes('dist/') ||
|
|
106
|
+
filePath.includes('build/') ||
|
|
107
|
+
filePath.includes('.DS_Store')
|
|
108
|
+
) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const isNew = statusCode.includes('?') || statusCode.includes('A');
|
|
113
|
+
const isStaged = statusCode[0] !== ' ' && statusCode[0] !== '?';
|
|
114
|
+
|
|
115
|
+
changes.push({
|
|
116
|
+
filePath,
|
|
117
|
+
isNew,
|
|
118
|
+
isStaged,
|
|
119
|
+
statusCode,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return changes;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 獲取檔案的變更內容
|
|
128
|
+
*/
|
|
129
|
+
static getFileDiff(filePath, isNew = false) {
|
|
130
|
+
try {
|
|
131
|
+
if (isNew) {
|
|
132
|
+
// 新檔案:讀取完整內容(前 100 行)
|
|
133
|
+
if (!existsSync(filePath)) {
|
|
134
|
+
return '[檔案不存在]';
|
|
135
|
+
}
|
|
136
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
137
|
+
const lines = content.split('\n').slice(0, 100);
|
|
138
|
+
return `[新檔案]\n${lines.join('\n')}${lines.length >= 100 ? '\n...' : ''}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 已存在檔案:獲取 diff
|
|
142
|
+
const diff = GitOperations.exec(`git diff HEAD -- "${filePath}"`, {
|
|
143
|
+
silent: true,
|
|
144
|
+
throwOnError: false,
|
|
145
|
+
});
|
|
146
|
+
return diff || '[無變更]';
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return `[讀取錯誤: ${error.message}]`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Add 檔案
|
|
154
|
+
*/
|
|
155
|
+
static addFile(filePath) {
|
|
156
|
+
GitOperations.exec(`git add "${filePath}"`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Add 多個檔案
|
|
161
|
+
*/
|
|
162
|
+
static addFiles(filePaths) {
|
|
163
|
+
for (const filePath of filePaths) {
|
|
164
|
+
GitOperations.addFile(filePath);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Reset staged 檔案
|
|
170
|
+
*/
|
|
171
|
+
static resetStaged() {
|
|
172
|
+
try {
|
|
173
|
+
GitOperations.exec('git reset HEAD -- .', { silent: true, throwOnError: false });
|
|
174
|
+
} catch {
|
|
175
|
+
// 忽略錯誤
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 執行 commit
|
|
181
|
+
*/
|
|
182
|
+
static async commit(message) {
|
|
183
|
+
// 使用臨時檔案避免 commit message 中的特殊字符問題
|
|
184
|
+
const { writeFileSync, unlinkSync } = await import('fs');
|
|
185
|
+
const tmpFile = '.git/COMMIT_EDITMSG_TMP';
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
writeFileSync(tmpFile, message, 'utf-8');
|
|
189
|
+
GitOperations.exec(`git commit -F ${tmpFile}`);
|
|
190
|
+
unlinkSync(tmpFile);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
try {
|
|
193
|
+
unlinkSync(tmpFile);
|
|
194
|
+
} catch {
|
|
195
|
+
// 忽略刪除臨時檔案的錯誤
|
|
196
|
+
}
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 獲取最近的 commits
|
|
203
|
+
*/
|
|
204
|
+
static getRecentCommits(count = 5) {
|
|
205
|
+
return GitOperations.exec(`git log -${count} --oneline`, { silent: true });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 獲取兩個分支之間的 diff
|
|
210
|
+
*/
|
|
211
|
+
static getDiffBetweenBranches(base, head) {
|
|
212
|
+
return GitOperations.exec(`git diff ${base}...${head}`, { silent: true });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 獲取兩個分支之間的 commit 列表
|
|
217
|
+
*/
|
|
218
|
+
static getCommitsBetweenBranches(base, head) {
|
|
219
|
+
const output = GitOperations.exec(`git log ${base}..${head} --oneline`, { silent: true });
|
|
220
|
+
return output.split('\n').filter(line => line.trim());
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 檢查分支是否存在
|
|
225
|
+
*/
|
|
226
|
+
static branchExists(branchName) {
|
|
227
|
+
try {
|
|
228
|
+
GitOperations.exec(`git rev-parse --verify ${branchName}`, {
|
|
229
|
+
silent: true,
|
|
230
|
+
throwOnError: true,
|
|
231
|
+
});
|
|
232
|
+
return true;
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 獲取檔案的 Git 歷史貢獻者
|
|
240
|
+
*/
|
|
241
|
+
static getFileContributors(filePath, depth = 20) {
|
|
242
|
+
try {
|
|
243
|
+
const output = GitOperations.exec(
|
|
244
|
+
`git log -${depth} --pretty=format:"%ae|%an" -- "${filePath}"`,
|
|
245
|
+
{ silent: true, throwOnError: false }
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const contributors = new Map();
|
|
249
|
+
const lines = output.split('\n').filter(line => line.trim());
|
|
250
|
+
|
|
251
|
+
for (const line of lines) {
|
|
252
|
+
const [email, name] = line.split('|');
|
|
253
|
+
if (email && name) {
|
|
254
|
+
contributors.set(email, name);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return Array.from(contributors.entries()).map(([email, name]) => ({ email, name }));
|
|
259
|
+
} catch {
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub API 工具
|
|
3
|
+
*
|
|
4
|
+
* 封裝 GitHub CLI 操作
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
|
|
9
|
+
export class GitHubAPI {
|
|
10
|
+
/**
|
|
11
|
+
* 執行 gh 命令
|
|
12
|
+
*/
|
|
13
|
+
static exec(command, options = {}) {
|
|
14
|
+
try {
|
|
15
|
+
return execSync(`gh ${command}`, {
|
|
16
|
+
encoding: 'utf-8',
|
|
17
|
+
stdio: options.silent ? 'pipe' : 'inherit',
|
|
18
|
+
...options,
|
|
19
|
+
}).toString();
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (options.throwOnError !== false) {
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 檢查 GitHub CLI 是否已安裝並已認證
|
|
30
|
+
*/
|
|
31
|
+
static isAvailable() {
|
|
32
|
+
try {
|
|
33
|
+
GitHubAPI.exec('--version', { silent: true });
|
|
34
|
+
GitHubAPI.exec('auth status', { silent: true });
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 創建 Pull Request
|
|
43
|
+
*/
|
|
44
|
+
static createPR(options = {}) {
|
|
45
|
+
const {
|
|
46
|
+
title,
|
|
47
|
+
body,
|
|
48
|
+
base,
|
|
49
|
+
head,
|
|
50
|
+
draft = false,
|
|
51
|
+
reviewers = [],
|
|
52
|
+
labels = [],
|
|
53
|
+
} = options;
|
|
54
|
+
|
|
55
|
+
let command = 'pr create';
|
|
56
|
+
|
|
57
|
+
if (title) command += ` --title "${title.replace(/"/g, '\\"')}"`;
|
|
58
|
+
if (body) command += ` --body "${body.replace(/"/g, '\\"')}"`;
|
|
59
|
+
if (base) command += ` --base "${base}"`;
|
|
60
|
+
if (head) command += ` --head "${head}"`;
|
|
61
|
+
if (draft) command += ' --draft';
|
|
62
|
+
|
|
63
|
+
if (reviewers.length > 0) {
|
|
64
|
+
command += ` --reviewer ${reviewers.join(',')}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (labels.length > 0) {
|
|
68
|
+
command += ` --label ${labels.join(',')}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return GitHubAPI.exec(command);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 獲取最新的 release 分支
|
|
76
|
+
*/
|
|
77
|
+
static getLatestReleaseBranch() {
|
|
78
|
+
try {
|
|
79
|
+
const output = GitHubAPI.exec('api repos/{owner}/{repo}/branches --jq ".[].name"', {
|
|
80
|
+
silent: true,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const branches = output.split('\n').filter(line => line.trim());
|
|
84
|
+
const releaseBranches = branches
|
|
85
|
+
.filter(branch => branch.startsWith('release-'))
|
|
86
|
+
.sort()
|
|
87
|
+
.reverse();
|
|
88
|
+
|
|
89
|
+
return releaseBranches[0] || null;
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 檢查 PR 是否已存在
|
|
97
|
+
*/
|
|
98
|
+
static prExists(base, head) {
|
|
99
|
+
try {
|
|
100
|
+
const output = GitHubAPI.exec(
|
|
101
|
+
`pr list --base ${base} --head ${head} --json number --jq "length"`,
|
|
102
|
+
{ silent: true, throwOnError: false }
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return parseInt(output.trim()) > 0;
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 獲取倉庫資訊
|
|
113
|
+
*/
|
|
114
|
+
static getRepoInfo() {
|
|
115
|
+
try {
|
|
116
|
+
const output = GitHubAPI.exec('repo view --json owner,name', { silent: true });
|
|
117
|
+
const data = JSON.parse(output);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
owner: data.owner?.login || null,
|
|
121
|
+
name: data.name || null,
|
|
122
|
+
};
|
|
123
|
+
} catch {
|
|
124
|
+
return { owner: null, name: null };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 獲取可用的 Labels
|
|
130
|
+
*/
|
|
131
|
+
static getLabels() {
|
|
132
|
+
try {
|
|
133
|
+
const output = GitHubAPI.exec('label list --json name --jq ".[].name"', { silent: true });
|
|
134
|
+
return output.split('\n').filter(line => line.trim());
|
|
135
|
+
} catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Git Tools - Main Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Export all core modules for programmatic usage
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { commitCommand } from './commands/commit.js';
|
|
8
|
+
export { commitAllCommand } from './commands/commit-all.js';
|
|
9
|
+
export { prCommand } from './commands/pr.js';
|
|
10
|
+
export { workflowCommand } from './commands/workflow.js';
|
|
11
|
+
export { initCommand } from './commands/init.js';
|
|
12
|
+
|
|
13
|
+
export { loadConfig } from './core/config-loader.js';
|
|
14
|
+
export { AIClient } from './core/ai-client.js';
|
|
15
|
+
export { GitOperations } from './core/git-operations.js';
|
|
16
|
+
export { GitHubAPI } from './core/github-api.js';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具函數
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 錯誤處理
|
|
9
|
+
*/
|
|
10
|
+
export function handleError(error) {
|
|
11
|
+
console.error(chalk.red('\n❌ 錯誤:'), error.message);
|
|
12
|
+
|
|
13
|
+
if (error.stack && process.env.DEBUG) {
|
|
14
|
+
console.error(chalk.gray(error.stack));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 驗證 commit message
|
|
20
|
+
*/
|
|
21
|
+
export function validateCommitMessage(message) {
|
|
22
|
+
if (!message || message.length === 0) {
|
|
23
|
+
return { valid: false, reason: 'Commit message 為空' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (message.length < 5) {
|
|
27
|
+
return { valid: false, reason: 'Commit message 太短(少於 5 個字元)' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 檢查是否只包含特殊字元或空白
|
|
31
|
+
if (!/[a-zA-Z0-9\u4e00-\u9fa5]/.test(message)) {
|
|
32
|
+
return { valid: false, reason: 'Commit message 不包含有效字元' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { valid: true };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 截斷長文本
|
|
40
|
+
*/
|
|
41
|
+
export function truncateText(text, maxLength) {
|
|
42
|
+
if (text.length <= maxLength) {
|
|
43
|
+
return text;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return text.substring(0, maxLength) + '\n\n... [文本已截斷]';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 格式化檔案列表
|
|
51
|
+
*/
|
|
52
|
+
export function formatFileList(files) {
|
|
53
|
+
return files.map((file, index) => {
|
|
54
|
+
const status = file.isNew ? '新增' : '修改';
|
|
55
|
+
return ` [${index}] ${status} - ${file.filePath}`;
|
|
56
|
+
}).join('\n');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 取得專案類型提示
|
|
61
|
+
*/
|
|
62
|
+
export function getProjectTypePrompt() {
|
|
63
|
+
return `你是一個資深前端工程師,熟悉現代 Web 開發規範。`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 取得 Conventional Commits 規則
|
|
68
|
+
*/
|
|
69
|
+
export function getConventionalCommitsRules() {
|
|
70
|
+
return `**Commit Message 規則**:
|
|
71
|
+
1. 使用 Conventional Commits 格式:type(scope): subject
|
|
72
|
+
2. type 必須是:feat/fix/docs/style/refactor/test/chore/perf 其中之一
|
|
73
|
+
3. scope: 影響範圍(選填,如 api、ui、config、auth 等)
|
|
74
|
+
4. subject 限制在 50 字內,使用繁體中文
|
|
75
|
+
5. 如果變更複雜,加上 body 說明(使用 bullet points)
|
|
76
|
+
|
|
77
|
+
**重要**:
|
|
78
|
+
- 直接輸出 commit message 純文字,不要使用 markdown 程式碼區塊(\`\`\`)
|
|
79
|
+
- 不要加上任何前綴說明或後綴文字
|
|
80
|
+
- 第一行是標題,如有需要可加上空行後的詳細說明`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 延遲執行
|
|
85
|
+
*/
|
|
86
|
+
export function delay(ms) {
|
|
87
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
88
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger 工具
|
|
3
|
+
*
|
|
4
|
+
* 提供格式化的日誌輸出
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import ora from 'ora';
|
|
9
|
+
|
|
10
|
+
export class Logger {
|
|
11
|
+
constructor(verbose = false) {
|
|
12
|
+
this.verbose = verbose;
|
|
13
|
+
this.spinner = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 標題
|
|
18
|
+
*/
|
|
19
|
+
header(text) {
|
|
20
|
+
console.log(chalk.cyan.bold(`\n${'='.repeat(60)}`));
|
|
21
|
+
console.log(chalk.cyan.bold(text));
|
|
22
|
+
console.log(chalk.cyan.bold('='.repeat(60)));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 成功訊息
|
|
27
|
+
*/
|
|
28
|
+
success(text) {
|
|
29
|
+
console.log(chalk.green('✅ ' + text));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 錯誤訊息
|
|
34
|
+
*/
|
|
35
|
+
error(text) {
|
|
36
|
+
console.log(chalk.red('❌ ' + text));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 警告訊息
|
|
41
|
+
*/
|
|
42
|
+
warn(text) {
|
|
43
|
+
console.log(chalk.yellow('⚠️ ' + text));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 資訊訊息
|
|
48
|
+
*/
|
|
49
|
+
info(text) {
|
|
50
|
+
console.log(chalk.blue('ℹ️ ' + text));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 除錯訊息(只在 verbose 模式顯示)
|
|
55
|
+
*/
|
|
56
|
+
debug(text) {
|
|
57
|
+
if (this.verbose) {
|
|
58
|
+
console.log(chalk.gray('🔍 ' + text));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 分隔線
|
|
64
|
+
*/
|
|
65
|
+
divider(char = '-', length = 50) {
|
|
66
|
+
console.log(char.repeat(length));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 程式碼區塊
|
|
71
|
+
*/
|
|
72
|
+
code(text) {
|
|
73
|
+
this.divider();
|
|
74
|
+
console.log(text);
|
|
75
|
+
this.divider();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 開始 spinner
|
|
80
|
+
*/
|
|
81
|
+
startSpinner(text) {
|
|
82
|
+
this.spinner = ora(text).start();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 更新 spinner 文字
|
|
87
|
+
*/
|
|
88
|
+
updateSpinner(text) {
|
|
89
|
+
if (this.spinner) {
|
|
90
|
+
this.spinner.text = text;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 停止 spinner(成功)
|
|
96
|
+
*/
|
|
97
|
+
succeedSpinner(text) {
|
|
98
|
+
if (this.spinner) {
|
|
99
|
+
this.spinner.succeed(text);
|
|
100
|
+
this.spinner = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 停止 spinner(失敗)
|
|
106
|
+
*/
|
|
107
|
+
failSpinner(text) {
|
|
108
|
+
if (this.spinner) {
|
|
109
|
+
this.spinner.fail(text);
|
|
110
|
+
this.spinner = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 顯示進度
|
|
116
|
+
*/
|
|
117
|
+
progress(current, total, text = '') {
|
|
118
|
+
const percentage = Math.round((current / total) * 100);
|
|
119
|
+
const bar = '█'.repeat(Math.round(percentage / 2));
|
|
120
|
+
const empty = '░'.repeat(50 - Math.round(percentage / 2));
|
|
121
|
+
console.log(`\r${bar}${empty} ${percentage}% ${text}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Git Tools 配置檔範本
|
|
3
|
+
*
|
|
4
|
+
* 此配置檔用於:
|
|
5
|
+
* - gitai commit:自動生成單個 commit
|
|
6
|
+
* - gitai commit-all:智能分析並批量 commit
|
|
7
|
+
* - gitai pr:自動生成 PR
|
|
8
|
+
* - gitai workflow:完整工作流程
|
|
9
|
+
*/
|
|
10
|
+
export default {
|
|
11
|
+
// AI 設定
|
|
12
|
+
ai: {
|
|
13
|
+
model: 'gpt-4.1', // AI 模型:gpt-4.1, claude-haiku-4.5, claude-sonnet-4.5
|
|
14
|
+
maxDiffLength: 8000, // 最大 diff 長度(字元)- 太小會導致 AI 看不到完整變更
|
|
15
|
+
maxRetries: 3, // API 失敗時的最大重試次數
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
// GitHub 設定(用於 PR 工具)
|
|
19
|
+
github: {
|
|
20
|
+
orgName: null, // GitHub 組織名稱(自動從 git remote 取得)
|
|
21
|
+
defaultBase: 'auto', // 預設目標分支:'auto' | 'main' | 'develop' | 'master'
|
|
22
|
+
autoLabels: true, // 自動添加 Labels
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
// Reviewer 設定(用於 PR 工具)
|
|
26
|
+
reviewers: {
|
|
27
|
+
autoSelect: false, // 是否啟用 reviewer 選擇功能(true: 啟用互動選擇 | false: 跳過選擇)
|
|
28
|
+
maxSuggested: 5, // 最多建議的 reviewers 數量(基於 Git 歷史分析)
|
|
29
|
+
gitHistoryDepth: 20, // Git 歷史分析深度(查看最近 N 筆 commit)
|
|
30
|
+
excludeAuthors: [], // 排除特定作者(email 或 username),例如: ['bot@', 'ci-user']
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// 輸出設定
|
|
34
|
+
output: {
|
|
35
|
+
verbose: false, // 顯示詳細輸出
|
|
36
|
+
saveHistory: false, // 儲存操作歷史(未來功能)
|
|
37
|
+
},
|
|
38
|
+
};
|