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.
@@ -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
+ };