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.
@@ -1,132 +0,0 @@
1
- import { existsSync } from 'fs';
2
- import { resolve } from 'path';
3
-
4
- /**
5
- * 解析命令列參數
6
- */
7
- export function parseCliArgs() {
8
- const args = process.argv.slice(2);
9
- const config = {
10
- baseBranch: null,
11
- headBranch: null,
12
- model: null,
13
- preview: false,
14
- noConfirm: false,
15
- interactiveReviewers: undefined,
16
- autoLabels: null,
17
- includeImpactAnalysis: null,
18
- };
19
-
20
- for (let i = 0; i < args.length; i++) {
21
- switch (args[i]) {
22
- case '--base':
23
- config.baseBranch = args[++i];
24
- break;
25
- case '--head':
26
- config.headBranch = args[++i];
27
- break;
28
- case '--model':
29
- config.model = args[++i];
30
- break;
31
- case '--preview':
32
- config.preview = true;
33
- break;
34
- case '--no-confirm':
35
- config.noConfirm = true;
36
- break;
37
- case '--interactive-reviewers':
38
- config.interactiveReviewers = true;
39
- break;
40
- case '--auto-labels':
41
- config.autoLabels = true;
42
- break;
43
- case '--include-impact':
44
- config.includeImpactAnalysis = true;
45
- break;
46
- case '--help':
47
- showHelp();
48
- process.exit(0);
49
- break;
50
- default:
51
- // 忽略未知的參數
52
- break;
53
- }
54
- }
55
-
56
- return config;
57
- }
58
-
59
- /**
60
- * 載入配置
61
- */
62
- export async function loadConfig() {
63
- // 1. 載入預設配置
64
- const configPath = resolve(process.cwd(), '.ai-git-config.mjs');
65
- let config = null;
66
-
67
- if (existsSync(configPath)) {
68
- const userConfig = await import(configPath);
69
- config = userConfig.default;
70
- } else {
71
- // 使用內建預設值
72
- config = {
73
- ai: { model: 'gpt-4.1', maxDiffLength: 8000, maxRetries: 3 },
74
- github: { defaultBase: 'release', autoLabels: true, includeImpactAnalysis: false },
75
- reviewers: { interactiveReviewers: true, maxSuggested: 5, gitHistoryDepth: 20, excludeAuthors: [] },
76
- output: { verbose: false, saveHistory: false },
77
- };
78
- }
79
-
80
- // 2. 合併命令列參數
81
- const cliConfig = parseCliArgs();
82
-
83
- // 合併配置(CLI 參數優先)
84
- if (cliConfig.model) config.ai.model = cliConfig.model;
85
- if (cliConfig.interactiveReviewers !== undefined) config.reviewers.interactiveReviewers = cliConfig.interactiveReviewers;
86
- if (cliConfig.autoLabels !== null) config.github.autoLabels = cliConfig.autoLabels;
87
- if (cliConfig.includeImpactAnalysis !== null) config.github.includeImpactAnalysis = cliConfig.includeImpactAnalysis;
88
-
89
- // 其他 CLI 參數直接加入 config
90
- config.baseBranch = cliConfig.baseBranch;
91
- config.headBranch = cliConfig.headBranch;
92
- config.preview = cliConfig.preview;
93
- config.noConfirm = cliConfig.noConfirm;
94
-
95
- return config;
96
- }
97
-
98
- /**
99
- * 顯示幫助訊息
100
- */
101
- function showHelp() {
102
- console.log(`
103
- 使用方式:
104
- npx ai-git-tools pr [選項]
105
-
106
- 選項:
107
- --base <branch> 指定目標分支 (預設: 使用配置檔的 defaultBase 或自動偵測)
108
- --model <model> 指定 AI 模型 (預設: gpt-4.1)
109
- --preview 僅預覽 PR 內容,不實際創建
110
- --no-confirm 跳過確認直接創建
111
- --interactive-reviewers 啟用互動式 reviewer 選擇 (預設啟用)
112
- --auto-labels 自動添加 Labels (預設啟用)
113
- --help 顯示此說明
114
-
115
- 範例:
116
- npx ai-git-tools pr
117
- npx ai-git-tools pr --base release-2025-m12.1
118
- npx ai-git-tools pr --preview
119
- npx ai-git-tools pr --no-confirm
120
-
121
- 配置檔範例 (.ai-git-config.js):
122
- export default {
123
- github: {
124
- defaultBase: 'release-2025-m12.1', // 指定預設目標分支
125
- autoLabels: true,
126
- },
127
- reviewers: {
128
- interactiveReviewers: true, // 啟用互動式選擇 reviewers
129
- },
130
- };
131
- `);
132
- }
@@ -1,248 +0,0 @@
1
- import { execSync } from 'child_process';
2
- import { CONSTANTS } from '../utils/constants.js';
3
- import { PRError } from '../utils/helpers.js';
4
-
5
- /**
6
- * Git 操作封裝
7
- */
8
- export class GitOperations {
9
- /**
10
- * 偵測可用的 release 分支
11
- */
12
- detectReleaseBranches() {
13
- try {
14
- // 嘗試同步遠端(含 --prune 以清除已刪除的遠端分支),失敗就用本機已知的遠端資訊
15
- try {
16
- execSync('git fetch --prune origin', { stdio: 'ignore', timeout: 15000 });
17
- } catch (_) {
18
- // fetch 失敗,繼續使用已經 cache 的遠端分支
19
- }
20
- const branches = execSync('git branch -r', { encoding: 'utf-8' })
21
- .split('\n')
22
- .map((b) => b.trim())
23
- .filter((b) => b.startsWith('origin/release-'))
24
- .map((b) => b.replace('origin/', ''));
25
- return branches;
26
- } catch (error) {
27
- return [];
28
- }
29
- }
30
-
31
- /**
32
- * 找到最新的 release 分支
33
- */
34
- findLatestReleaseBranch() {
35
- const branches = this.detectReleaseBranches();
36
- if (branches.length === 0) return null;
37
-
38
- // 優先選月分支(-m),其次週分支(-w),最後 fallback 到全部 release 分支
39
- const monthlyBranches = branches.filter((b) => b.includes('-m'));
40
- const weeklyBranches = branches.filter((b) => b.includes('-w'));
41
- const priorityBranches =
42
- monthlyBranches.length > 0
43
- ? monthlyBranches
44
- : weeklyBranches.length > 0
45
- ? weeklyBranches
46
- : branches;
47
-
48
- priorityBranches.sort().reverse();
49
- return priorityBranches[0] || null;
50
- }
51
-
52
- /**
53
- * 獲取當前分支
54
- */
55
- getCurrentBranch() {
56
- return execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
57
- }
58
-
59
- /**
60
- * 獲取變更統計
61
- */
62
- getChangeStats(baseBranch, headBranch, useRemoteHead = false) {
63
- try {
64
- // 使用本地 headBranch 以確保能檢測到未推送的變更
65
- const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
66
- const stats = execSync(`git diff --shortstat origin/${baseBranch}...${headRef}`, {
67
- encoding: 'utf-8',
68
- }).trim();
69
-
70
- const filesChanged = execSync(
71
- `git diff --name-only origin/${baseBranch}...${headRef} | wc -l`,
72
- { encoding: 'utf-8' }
73
- ).trim();
74
-
75
- return { stats, filesChanged: parseInt(filesChanged, 10) };
76
- } catch (error) {
77
- return { stats: '無法獲取統計', filesChanged: 0 };
78
- }
79
- }
80
-
81
- /**
82
- * 獲取變更的檔案列表
83
- */
84
- getChangedFiles(baseBranch, headBranch, useRemoteHead = false) {
85
- try {
86
- // 使用本地 headBranch 以確保能檢測到未推送的變更
87
- const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
88
- const files = execSync(`git diff --name-only origin/${baseBranch}...${headRef}`, {
89
- encoding: 'utf-8',
90
- })
91
- .split('\n')
92
- .filter(Boolean);
93
- return files;
94
- } catch (error) {
95
- return [];
96
- }
97
- }
98
-
99
- /**
100
- * 獲取 commit 列表
101
- */
102
- getCommits(baseBranch, headBranch, options = {}) {
103
- const { oneline = true, noDecorate = true, useRemoteHead = false } = options;
104
- try {
105
- // 使用本地 headBranch 以確保能檢測到未推送的 commit
106
- const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
107
- let cmd = `git log origin/${baseBranch}..${headRef}`;
108
- if (oneline) cmd += ' --oneline';
109
- if (noDecorate) cmd += ' --no-decorate';
110
-
111
- return execSync(cmd, { encoding: 'utf-8' });
112
- } catch (error) {
113
- throw new PRError(
114
- '無法比較分支差異',
115
- 'GIT_COMPARE_FAILED',
116
- ['檢查遠端分支是否存在: git branch -r', '執行診斷: npm run diagnose:pr'],
117
- `git log origin/${baseBranch}..${headBranch}`
118
- );
119
- }
120
- }
121
-
122
- /**
123
- * 獲取 diff
124
- */
125
- getDiff(baseBranch, headBranch, useRemoteHead = false, maxBuffer = CONSTANTS.MAX_BUFFER_SIZE) {
126
- try {
127
- // 使用本地 headBranch 以確保能檢測到未推送的變更
128
- const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
129
- return execSync(`git diff origin/${baseBranch}...${headRef}`, {
130
- encoding: 'utf-8',
131
- maxBuffer,
132
- });
133
- } catch (error) {
134
- // 嘗試替代方案
135
- const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
136
- return execSync(`git diff origin/${baseBranch}..${headRef}`, {
137
- encoding: 'utf-8',
138
- maxBuffer,
139
- });
140
- }
141
- }
142
-
143
- /**
144
- * 推送到遠端
145
- */
146
- async push(branch) {
147
- try {
148
- execSync(`git push -u origin ${branch}`, {
149
- stdio: ['ignore', 'inherit', 'pipe'],
150
- encoding: 'utf-8',
151
- });
152
- return true;
153
- } catch (error) {
154
- const errMsg = (error.stderr || error.message || '').toString();
155
- const is403 = errMsg.includes('403') || errMsg.includes('Write access') || errMsg.includes('write access');
156
- if (is403) {
157
- throw new PRError(
158
- '\u63a8送失敗:git 沒有寫入權限',
159
- 'GIT_PUSH_FORBIDDEN',
160
- [
161
- '建議執行以下指令讓 git 使用 gh 的認證:',
162
- ' gh auth setup-git',
163
- '或者改用 SSH 權限:',
164
- ' git remote set-url origin git@github.com:<org>/<repo>.git',
165
- ],
166
- `git push -u origin ${branch}`
167
- );
168
- }
169
- throw new PRError(
170
- '推送失敗',
171
- 'GIT_PUSH_FAILED',
172
- ['檢查是否有推送權限', '檢查遠端分支是否有衝突', '檢查網路連接'],
173
- `git push -u origin ${branch}`
174
- );
175
- }
176
- }
177
-
178
- /**
179
- * 同步遠端資訊
180
- */
181
- async fetch() {
182
- try {
183
- execSync('git fetch origin', { stdio: 'ignore' });
184
- return true;
185
- } catch (error) {
186
- return false;
187
- }
188
- }
189
-
190
- /**
191
- * 智能截斷 diff
192
- */
193
- truncateDiff(diff, maxLength = CONSTANTS.MAX_DIFF_LENGTH) {
194
- if (diff.length <= maxLength) return diff;
195
-
196
- const lines = diff.split('\n');
197
- const header = lines.slice(0, CONSTANTS.DIFF_CONTEXT_LINES).join('\n');
198
- const footer = lines.slice(-CONSTANTS.DIFF_CONTEXT_LINES).join('\n');
199
-
200
- const middle = `\n\n... [已省略 ${lines.length - 100} 行變更] ...\n\n`;
201
-
202
- return header + middle + footer;
203
- }
204
-
205
- /**
206
- * 獲取當前用戶資訊
207
- */
208
- getCurrentUser() {
209
- try {
210
- const email = execSync('git config user.email', { encoding: 'utf-8' }).trim();
211
- const name = execSync('git config user.name', { encoding: 'utf-8' }).trim();
212
- let githubUser = null;
213
-
214
- try {
215
- githubUser = execSync('gh api user --jq .login', {
216
- encoding: 'utf-8',
217
- stdio: ['pipe', 'pipe', 'pipe'],
218
- }).trim();
219
- } catch {
220
- // GitHub CLI 未認證或未安裝
221
- }
222
-
223
- return { email, name, githubUser };
224
- } catch (error) {
225
- return null;
226
- }
227
- }
228
-
229
- /**
230
- * 獲取 repository owner
231
- */
232
- getRepoOwner() {
233
- try {
234
- const remoteUrl = execSync('git config --get remote.origin.url', {
235
- encoding: 'utf-8',
236
- }).trim();
237
-
238
- // 解析 GitHub URL
239
- const httpsMatch = remoteUrl.match(/github\.com[/:]([^/]+)\//);
240
- const sshMatch = remoteUrl.match(/github\.com:([^/]+)\//);
241
-
242
- const owner = httpsMatch?.[1] || sshMatch?.[1];
243
- return owner || null;
244
- } catch (error) {
245
- return null;
246
- }
247
- }
248
- }
@@ -1,40 +0,0 @@
1
- import { colors } from '../utils/constants.js';
2
-
3
- /**
4
- * 日誌輸出工具
5
- */
6
- export class Logger {
7
- info(msg) {
8
- console.log(`${colors.blue}ℹ${colors.reset} ${msg}`);
9
- }
10
-
11
- success(msg) {
12
- console.log(`${colors.green}✅${colors.reset} ${msg}`);
13
- }
14
-
15
- warning(msg) {
16
- console.log(`${colors.yellow}⚠️${colors.reset} ${msg}`);
17
- }
18
-
19
- error(msg) {
20
- console.log(`${colors.red}❌${colors.reset} ${msg}`);
21
- }
22
-
23
- step(msg) {
24
- console.log(`${colors.cyan}▶${colors.reset} ${msg}`);
25
- }
26
-
27
- header(msg) {
28
- console.log(`\n${colors.bright}🤖 ${msg}${colors.reset}\n`);
29
- }
30
-
31
- separator(char = '═', length = 80) {
32
- console.log(char.repeat(length));
33
- }
34
-
35
- section(title) {
36
- console.log(`\n${'═'.repeat(80)}`);
37
- console.log(`${colors.bright}${title}${colors.reset}`);
38
- console.log('═'.repeat(80));
39
- }
40
- }
@@ -1,75 +0,0 @@
1
- import { colors } from './constants.js';
2
-
3
- /**
4
- * 日誌工具
5
- */
6
- export const log = {
7
- info: (msg) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`),
8
- success: (msg) => console.log(`${colors.green}✅${colors.reset} ${msg}`),
9
- warning: (msg) => console.log(`${colors.yellow}⚠️${colors.reset} ${msg}`),
10
- error: (msg) => console.log(`${colors.red}❌${colors.reset} ${msg}`),
11
- step: (msg) => console.log(`${colors.cyan}▶${colors.reset} ${msg}`),
12
- };
13
-
14
- /**
15
- * 自訂錯誤類別
16
- */
17
- export class PRError extends Error {
18
- constructor(message, code, suggestions = [], diagnosticCommand = null) {
19
- super(message);
20
- this.name = 'PRError';
21
- this.code = code;
22
- this.suggestions = suggestions;
23
- this.diagnosticCommand = diagnosticCommand;
24
- }
25
- }
26
-
27
- /**
28
- * 錯誤處理器
29
- */
30
- export function handleError(error) {
31
- if (error instanceof PRError) {
32
- log.error(error.message);
33
-
34
- if (error.suggestions && error.suggestions.length > 0) {
35
- console.log(`\n${colors.cyan}💡 建議解決方案:${colors.reset}`);
36
- error.suggestions.forEach((suggestion, index) => {
37
- console.log(` ${index + 1}. ${suggestion}`);
38
- });
39
- }
40
-
41
- if (error.diagnosticCommand) {
42
- console.log(`\n${colors.yellow}🔍 診斷命令:${colors.reset}`);
43
- console.log(` ${error.diagnosticCommand}`);
44
- }
45
- } else {
46
- log.error(`錯誤: ${error.message}`);
47
- if (error.stack) {
48
- console.error(error.stack);
49
- }
50
- }
51
- }
52
-
53
- /**
54
- * 取得 skills 規則的文字摘要(供 AI prompt 使用)
55
- */
56
- export function getSkillsSummaryForPrompt(PROJECT_SKILLS_CONTEXT) {
57
- const rbp = PROJECT_SKILLS_CONTEXT.reactBestPractices
58
- .map((r) => ` - [${r.id}] ${r.category}: ${r.desc}`)
59
- .join('\n');
60
- const fg = PROJECT_SKILLS_CONTEXT.frontendGuidelines;
61
- return `
62
- ## 專案規範(來自 .github/skills/)
63
-
64
- ### React Best Practices 規則(共 ${PROJECT_SKILLS_CONTEXT.reactBestPractices.length} 條核心規則)
65
- ${rbp}
66
-
67
- ### Frontend Guidelines 規範
68
- - 架構: ${fg.architecture}
69
- - 元件命名: ${fg.naming.component}
70
- - 工具檔案: ${fg.naming.utility}
71
- - 變數: ${fg.naming.variable} / 常數: ${fg.naming.constant} / 布林值: ${fg.naming.boolean}
72
- - Import 順序: ${fg.importOrder}
73
- - 狀態管理: 客戶端=${fg.stateManagement.client} | 伺服器=${fg.stateManagement.server} | 表單=${fg.stateManagement.form}
74
- `;
75
- }