ai-git-tools 2.0.8 → 2.0.10

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,371 @@
1
+ import { createInterface } from 'readline';
2
+ import { GitOperations } from './git-operations.js';
3
+ import { GitHubAPI } from './github-api.js';
4
+ import { AIAnalyzer } from '../ai/code-analyzer.js';
5
+ import { LabelAnalyzer } from '../ai/label-analyzer.js';
6
+ import { ReviewerSelector } from '../reviewers/reviewer-selector.js';
7
+ import { Logger } from '../ui/logger.js';
8
+ import { PRError, log } from '../utils/helpers.js';
9
+ import { CONSTANTS, colors } from '../utils/constants.js';
10
+
11
+ /**
12
+ * PR 工作流程編排
13
+ */
14
+ export class PRWorkflow {
15
+ constructor(config) {
16
+ this.config = config;
17
+ this.git = new GitOperations();
18
+ this.github = new GitHubAPI({ orgName: config.github.orgName });
19
+ this.ai = new AIAnalyzer({ model: config.ai.model });
20
+ this.labelAnalyzer = new LabelAnalyzer();
21
+ this.reviewerSelector = new ReviewerSelector({
22
+ autoReviewers: config.reviewers.autoSelect,
23
+ maxSuggested: config.reviewers.maxSuggested,
24
+ gitHistoryDepth: config.reviewers.gitHistoryDepth,
25
+ excludeAuthors: config.reviewers.excludeAuthors,
26
+ });
27
+ this.logger = new Logger();
28
+ }
29
+
30
+ /**
31
+ * 執行完整工作流程
32
+ */
33
+ async execute() {
34
+ // 1. 驗證環境和分支
35
+ const { baseBranch, headBranch } = await this.detectAndValidateBranches();
36
+
37
+ // 2. 檢查是否有變更
38
+ await this.validateChanges(baseBranch, headBranch);
39
+
40
+ // 3. 推送到遠端(預覽模式跳過)
41
+ if (!this.config.preview) {
42
+ await this.pushToRemote(headBranch);
43
+ }
44
+
45
+ // 4. 收集變更資訊
46
+ const changeData = this.collectChangeData(baseBranch, headBranch);
47
+
48
+ // 5. AI 分析和生成 PR 內容
49
+ const prContent = await this.generatePRContent(changeData);
50
+
51
+ // 6. 顯示預覽
52
+ this.displayPreview(prContent, changeData.stats);
53
+
54
+ // 預覽模式:僅顯示不創建
55
+ if (this.config.preview) {
56
+ log.info('預覽模式:未創建 PR');
57
+ return;
58
+ }
59
+
60
+ // 7. 選擇 Reviewers
61
+ const reviewers = await this.selectReviewers(changeData);
62
+
63
+ // 8. 確認創建
64
+ if (!this.config.noConfirm) {
65
+ const confirmed = await this.askConfirmation('是否創建此 Pull Request?');
66
+ if (!confirmed) {
67
+ log.info('已取消創建 PR');
68
+ return;
69
+ }
70
+ }
71
+
72
+ // 9. 創建 PR
73
+ const prUrl = await this.createPR(prContent, baseBranch, headBranch, reviewers);
74
+
75
+ // 10. 添加 Labels(如果啟用)
76
+ if (this.config.github.autoLabels && prUrl) {
77
+ const prNumber = prUrl.split('/').pop();
78
+ await this.addLabels(prNumber, {
79
+ ...prContent,
80
+ stats: changeData.stats,
81
+ });
82
+ }
83
+
84
+ this.logger.success('完成!');
85
+ }
86
+
87
+ /**
88
+ * 偵測和驗證分支
89
+ */
90
+ async detectAndValidateBranches() {
91
+ // 獲取當前分支
92
+ const currentBranch = this.git.getCurrentBranch();
93
+ let headBranch = this.config.headBranch || currentBranch;
94
+ let baseBranch = this.config.baseBranch;
95
+
96
+ // 自動偵測 base branch
97
+ if (!baseBranch) {
98
+ log.step('正在偵測 release 分支...\n');
99
+ const allBranches = this.git.detectReleaseBranches();
100
+ const latestRelease = this.git.findLatestReleaseBranch();
101
+
102
+ if (latestRelease) {
103
+ baseBranch = latestRelease;
104
+ this.displayDetectedBranches(allBranches, latestRelease);
105
+ log.success(`自動選擇最新分支: ${baseBranch}`);
106
+ log.info(`提示: 使用 --base <分支名> 指定其他分支\n`);
107
+ } else {
108
+ throw new PRError('未偵測到任何 release 分支', 'NO_RELEASE_BRANCH', [
109
+ '使用 --base 參數指定目標分支',
110
+ '確認遠端分支存在: git branch -r | grep release',
111
+ ]);
112
+ }
113
+ }
114
+
115
+ // 檢查是否在 base branch
116
+ if (currentBranch === baseBranch) {
117
+ throw new PRError(`你目前在 ${baseBranch} 分支,請切換到 feature 分支`, 'ON_BASE_BRANCH', [
118
+ '切換到 feature 分支: git checkout <feature-branch>',
119
+ ]);
120
+ }
121
+
122
+ console.log(`📊 準備創建 PR: ${headBranch} → ${baseBranch}\n`);
123
+
124
+ return { baseBranch, headBranch };
125
+ }
126
+
127
+ /**
128
+ * 顯示偵測到的分支
129
+ */
130
+ displayDetectedBranches(allBranches, latestRelease) {
131
+ const monthlyBranches = allBranches.filter((b) => b.includes('-m'));
132
+ const weeklyBranches = allBranches.filter((b) => b.includes('-w'));
133
+
134
+ console.log('📋 偵測到的 release 分支:\n');
135
+
136
+ if (monthlyBranches.length > 0) {
137
+ console.log(' 月度分支 (優先):');
138
+ monthlyBranches.slice(0, 3).forEach((branch, index) => {
139
+ const marker = branch === latestRelease ? ' ← 最新' : '';
140
+ console.log(` ${index + 1}. ${branch}${marker}`);
141
+ });
142
+ if (monthlyBranches.length > 3) {
143
+ console.log(` ... 還有 ${monthlyBranches.length - 3} 個月度分支`);
144
+ }
145
+ }
146
+
147
+ if (weeklyBranches.length > 0) {
148
+ console.log('\n 週度分支:');
149
+ weeklyBranches.slice(0, 3).forEach((branch, index) => {
150
+ const marker = branch === latestRelease ? ' ← 最新' : '';
151
+ console.log(` ${index + 1}. ${branch}${marker}`);
152
+ });
153
+ if (weeklyBranches.length > 3) {
154
+ console.log(` ... 還有 ${weeklyBranches.length - 3} 個週度分支`);
155
+ }
156
+ }
157
+
158
+ console.log('');
159
+ }
160
+
161
+ /**
162
+ * 驗證變更
163
+ */
164
+ async validateChanges(baseBranch, headBranch) {
165
+ // 先同步遠端資訊
166
+ log.step('正在同步遠端資訊...');
167
+ await this.git.fetch();
168
+ log.success('同步完成\n');
169
+
170
+ // 檢查本地 commit 差異
171
+ let localCommits;
172
+ try {
173
+ localCommits = this.git.getCommits(baseBranch, headBranch, {
174
+ oneline: true,
175
+ noDecorate: true,
176
+ });
177
+ } catch (error) {
178
+ throw new PRError(
179
+ `無法比較分支差異,請確認 origin/${baseBranch} 分支存在`,
180
+ 'BRANCH_COMPARE_FAILED',
181
+ ['同步遠端: git fetch origin', `檢查分支: git branch -r | grep ${baseBranch}`]
182
+ );
183
+ }
184
+
185
+ if (!localCommits.trim()) {
186
+ throw new PRError(`${headBranch} 和 origin/${baseBranch} 之間沒有新的 commit`, 'NO_COMMITS', [
187
+ '檢查當前分支: git branch',
188
+ `查看分支歷史: git log origin/${baseBranch}..${headBranch}`,
189
+ '確認是否有未提交的變更: git status',
190
+ ]);
191
+ }
192
+
193
+ // 顯示 commit 預覽
194
+ console.log(`${colors.cyan}📝 本地 commit 預覽(相對於 origin/${baseBranch}):${colors.reset}`);
195
+ const commitLines = localCommits.split('\n');
196
+ console.log(
197
+ commitLines
198
+ .slice(0, CONSTANTS.MAX_COMMIT_PREVIEW)
199
+ .map((line) => ` ${line}`)
200
+ .join('\n')
201
+ );
202
+ if (commitLines.length > CONSTANTS.MAX_COMMIT_PREVIEW) {
203
+ console.log(` ... 還有 ${commitLines.length - CONSTANTS.MAX_COMMIT_PREVIEW} 個 commit`);
204
+ }
205
+ console.log('');
206
+ }
207
+
208
+ /**
209
+ * 推送到遠端
210
+ */
211
+ async pushToRemote(headBranch) {
212
+ log.step(`推送到遠端分支: origin/${headBranch}`);
213
+ await this.git.push(headBranch);
214
+ log.success('推送成功\n');
215
+
216
+ // 等待 GitHub 同步
217
+ log.info('等待 GitHub 同步...');
218
+ await new Promise((resolve) => setTimeout(resolve, CONSTANTS.GITHUB_SYNC_DELAY));
219
+
220
+ // 重新同步
221
+ await this.git.fetch();
222
+ log.success('同步完成\n');
223
+ }
224
+
225
+ /**
226
+ * 收集變更資料
227
+ */
228
+ collectChangeData(baseBranch, headBranch) {
229
+ const stats = this.git.getChangeStats(baseBranch, headBranch);
230
+ const changedFiles = this.git.getChangedFiles(baseBranch, headBranch);
231
+ const commits = this.git.getCommits(baseBranch, headBranch);
232
+ const diff = this.git.getDiff(baseBranch, headBranch);
233
+ const truncatedDiff = this.git.truncateDiff(diff);
234
+
235
+ console.log(`📈 變更統計: ${stats.stats}`);
236
+ console.log(`📁 影響檔案: ${stats.filesChanged} 個\n`);
237
+
238
+ if (truncatedDiff.length < diff.length) {
239
+ log.info(`變更內容較大,已智能截斷 (${diff.length} → ${truncatedDiff.length} 字元)\n`);
240
+ }
241
+
242
+ return { stats, changedFiles, commits, diff: truncatedDiff };
243
+ }
244
+
245
+ /**
246
+ * 生成 PR 內容
247
+ */
248
+ async generatePRContent(changeData) {
249
+ log.step(`正在使用 AI 生成 PR 內容 (${this.config.ai.model})...\n`);
250
+
251
+ // 生成 PR 標題和描述
252
+ const { title, body } = await this.ai.generatePRContent(changeData.commits, changeData.diff);
253
+
254
+ // 分析影響範圍
255
+ log.step('正在使用 AI 深度分析影響範圍和潛在問題...\n');
256
+ const { blastRadius, warnings } = await this.ai.analyzeImpact(
257
+ changeData.changedFiles,
258
+ changeData.diff,
259
+ changeData.commits
260
+ );
261
+
262
+ // 顯示分析結果摘要
263
+ this.displayAnalysisSummary(blastRadius, warnings);
264
+
265
+ // 將分析結果附加到 body
266
+ const enhancedBody = this.ai.appendAnalysisToBody(body, blastRadius, warnings);
267
+
268
+ return { title, body: enhancedBody, blastRadius, warnings };
269
+ }
270
+
271
+ /**
272
+ * 顯示分析結果摘要
273
+ */
274
+ displayAnalysisSummary(blastRadius, warnings) {
275
+ console.log(`${colors.cyan}📊 AI 分析結果:${colors.reset}`);
276
+ if (blastRadius.modules.length > 0) {
277
+ console.log(` • 影響模組: ${blastRadius.modules.join('、')}`);
278
+ }
279
+ if (blastRadius.impacts.length > 0) {
280
+ console.log(` • 影響層面: ${blastRadius.impacts.join('、')}`);
281
+ }
282
+ const riskEmojiMap = { 高: '🔴', 中: '🟡', 低: '🟢' };
283
+ const riskEmoji = riskEmojiMap[blastRadius.riskLevel] || '🟢';
284
+ console.log(` • 風險等級: ${riskEmoji} ${blastRadius.riskLevel}`);
285
+ if (warnings.length > 0) {
286
+ console.log(` • 發現 ${warnings.length} 個注意事項`);
287
+ }
288
+ console.log('');
289
+ }
290
+
291
+ /**
292
+ * 顯示 PR 預覽
293
+ */
294
+ displayPreview(prContent, stats) {
295
+ console.log(`\n${'═'.repeat(80)}`);
296
+ console.log(`${colors.bright}📋 PR 預覽${colors.reset}`);
297
+ console.log('═'.repeat(80));
298
+ console.log(`\n${colors.cyan}標題:${colors.reset} ${prContent.title}\n`);
299
+ console.log(`${colors.cyan}統計:${colors.reset} ${stats.stats}`);
300
+ console.log(`${colors.cyan}檔案數:${colors.reset} ${stats.filesChanged} 個檔案\n`);
301
+ console.log('─'.repeat(80));
302
+ console.log(`${colors.cyan}描述:${colors.reset}\n`);
303
+ console.log(prContent.body);
304
+ console.log(`\n${'═'.repeat(80)}\n`);
305
+ }
306
+
307
+ /**
308
+ * 選擇 Reviewers
309
+ */
310
+ async selectReviewers(changeData) {
311
+ if (this.config.noConfirm) {
312
+ return null;
313
+ }
314
+
315
+ // 獲取當前用戶
316
+ const currentUser = this.git.getCurrentUser();
317
+
318
+ // 抓取 GitHub 團隊資訊
319
+ const teams = await this.github.fetchTeams();
320
+
321
+ // 根據 Git History 建議 Reviewers
322
+ const suggestedReviewers = this.reviewerSelector.getReviewersByGitHistory(
323
+ changeData.changedFiles
324
+ );
325
+
326
+ // 選擇 Reviewers
327
+ return await this.reviewerSelector.select(teams, suggestedReviewers, currentUser);
328
+ }
329
+
330
+ /**
331
+ * 創建 PR
332
+ */
333
+ async createPR(prContent, baseBranch, headBranch, reviewers) {
334
+ return await this.github.createOrUpdatePR({
335
+ title: prContent.title,
336
+ body: prContent.body,
337
+ baseBranch,
338
+ headBranch,
339
+ reviewers,
340
+ config: this.config,
341
+ });
342
+ }
343
+
344
+ /**
345
+ * 添加 Labels
346
+ */
347
+ async addLabels(prNumber, prData) {
348
+ log.step('正在分析並添加 Labels...');
349
+ const labels = await this.labelAnalyzer.analyzeAndApply(prNumber, prData);
350
+ if (labels.length > 0) {
351
+ console.log(`📌 已添加 Labels: ${labels.join(', ')}\n`);
352
+ }
353
+ }
354
+
355
+ /**
356
+ * 互動式確認
357
+ */
358
+ askConfirmation(question) {
359
+ return new Promise((resolve) => {
360
+ const rl = createInterface({
361
+ input: process.stdin,
362
+ output: process.stdout,
363
+ });
364
+
365
+ rl.question(`${question} (y/N): `, (answer) => {
366
+ rl.close();
367
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
368
+ });
369
+ });
370
+ }
371
+ }
@@ -0,0 +1,232 @@
1
+ import { execSync } from 'child_process';
2
+ import { CONSTANTS, colors } from '../utils/constants.js';
3
+ import { log } from '../utils/helpers.js';
4
+ import { InteractiveSelect } from '../ui/interactive-select.js';
5
+
6
+ /**
7
+ * Reviewer 選擇器
8
+ */
9
+ export class ReviewerSelector {
10
+ constructor(config = {}) {
11
+ this.interactiveReviewers = config.interactiveReviewers !== undefined ? config.interactiveReviewers : true;
12
+ this.maxSuggested = config.maxSuggested || 5;
13
+ this.gitHistoryDepth = config.gitHistoryDepth || 20;
14
+ this.excludeAuthors = config.excludeAuthors || [];
15
+ }
16
+
17
+ /**
18
+ * 根據 Git History 找出最常修改這些檔案的人
19
+ */
20
+ getReviewersByGitHistory(changedFiles, limit = null) {
21
+ const contributors = {};
22
+ const actualLimit = limit || this.maxSuggested;
23
+
24
+ changedFiles.forEach((file) => {
25
+ try {
26
+ const logOutput = execSync(
27
+ `git log -${this.gitHistoryDepth} --format="%ae|%an" -- "${file}"`,
28
+ {
29
+ encoding: 'utf-8',
30
+ stdio: ['pipe', 'pipe', 'pipe'],
31
+ }
32
+ );
33
+
34
+ logOutput
35
+ .split('\n')
36
+ .filter(Boolean)
37
+ .forEach((line) => {
38
+ const [email, name] = line.split('|');
39
+ if (email && email.includes('@')) {
40
+ // 檢查是否在排除列表中
41
+ const shouldExclude = this.excludeAuthors.some((excluded) => {
42
+ const normalizedExcluded = excluded.toLowerCase();
43
+ return (
44
+ email.toLowerCase().includes(normalizedExcluded) ||
45
+ (name && name.toLowerCase().includes(normalizedExcluded))
46
+ );
47
+ });
48
+
49
+ if (!shouldExclude) {
50
+ const key = email.toLowerCase();
51
+ contributors[key] = {
52
+ email,
53
+ name: name || email.split('@')[0],
54
+ commits: (contributors[key]?.commits || 0) + 1,
55
+ };
56
+ }
57
+ }
58
+ });
59
+ } catch (error) {
60
+ // 忽略單個檔案的錯誤
61
+ }
62
+ });
63
+
64
+ // 排序並取前 N 名
65
+ return Object.values(contributors)
66
+ .sort((a, b) => b.commits - a.commits)
67
+ .slice(0, actualLimit);
68
+ }
69
+
70
+ /**
71
+ * 互動式選擇 Reviewers
72
+ */
73
+ async selectInteractive(teamsData, suggestedReviewers, currentUser) {
74
+ console.log(`\n${'═'.repeat(80)}`);
75
+ console.log(`${colors.bright}🎯 選擇 Reviewers${colors.reset}`);
76
+ console.log(`${'═'.repeat(80)}\n`);
77
+
78
+ // 解構 teams 和 members
79
+ const teams = teamsData.teams || {};
80
+ const orgMembers = teamsData.members || [];
81
+
82
+ // 顯示建議的 Reviewers(基於 Git History)
83
+ if (suggestedReviewers && suggestedReviewers.length > 0) {
84
+ console.log(`${colors.cyan}💡 建議 Reviewers(基於 Git 歷史):${colors.reset}`);
85
+ suggestedReviewers.forEach((reviewer, idx) => {
86
+ if (currentUser && reviewer.email.toLowerCase() === currentUser.email.toLowerCase()) {
87
+ return;
88
+ }
89
+ console.log(
90
+ ` ${idx + 1}. ${colors.green}${reviewer.name}${colors.reset} (${reviewer.email}) - ${
91
+ reviewer.commits
92
+ } commits`
93
+ );
94
+ });
95
+ console.log('');
96
+ }
97
+
98
+ // 準備選項列表
99
+ const options = [];
100
+
101
+ // 加入團隊選項
102
+ const teamsList = Object.values(teams);
103
+ teamsList.forEach((team) => {
104
+ const memberCount = team.members.length;
105
+ const memberNames = team.members
106
+ .slice(0, 3)
107
+ .map((m) => m.name)
108
+ .join(', ');
109
+ const moreText = memberCount > 3 ? ` ... +${memberCount - 3} 人` : '';
110
+
111
+ options.push({
112
+ type: 'team',
113
+ slug: team.slug,
114
+ label: `👥 ${team.name}`,
115
+ extra: ` ${colors.blue}(${team.slug} - ${memberCount} 位成員: ${memberNames}${moreText})${colors.reset}`,
116
+ });
117
+ });
118
+
119
+ // 收集所有個人成員
120
+ const allMembersMap = new Map();
121
+
122
+ Object.values(teams).forEach((team) => {
123
+ team.members.forEach((member) => {
124
+ if (!currentUser || member.login !== currentUser.githubUser) {
125
+ allMembersMap.set(member.login, member);
126
+ }
127
+ });
128
+ });
129
+
130
+ orgMembers.forEach((member) => {
131
+ if (!currentUser || member.login !== currentUser.githubUser) {
132
+ allMembersMap.set(member.login, member);
133
+ }
134
+ });
135
+
136
+ const uniqueMembers = Array.from(allMembersMap.values()).sort((a, b) =>
137
+ a.name.localeCompare(b.name)
138
+ );
139
+
140
+ // 加入個人成員選項
141
+ uniqueMembers.forEach((member) => {
142
+ options.push({
143
+ type: 'individual',
144
+ login: member.login,
145
+ label: `👤 @${member.login}`,
146
+ extra: ` ${colors.magenta}(${member.name})${colors.reset}`,
147
+ });
148
+ });
149
+
150
+ if (options.length === 0) {
151
+ console.log(`${colors.yellow}未找到可用的 reviewers${colors.reset}`);
152
+ console.log(`${colors.blue}提示: 你仍可以在創建 PR 後手動添加 reviewers${colors.reset}\n`);
153
+ return { teams: [], individuals: [] };
154
+ }
155
+
156
+ console.log(
157
+ `${colors.green}找到 ${teamsList.length} 個團隊和 ${uniqueMembers.length} 位成員${colors.reset}\n`
158
+ );
159
+
160
+ // 使用互動式選擇
161
+ const selector = new InteractiveSelect();
162
+ const result = await selector.select(options, '🎯 選擇 Reviewers');
163
+
164
+ if (result.cancelled || result.selected.length === 0) {
165
+ console.log(`${colors.yellow}未選擇任何 reviewer${colors.reset}\n`);
166
+ return { teams: [], individuals: [] };
167
+ }
168
+
169
+ // 分類選中的項目
170
+ const selectedTeams = [];
171
+ const selectedIndividuals = [];
172
+
173
+ result.selected.forEach((item) => {
174
+ if (item.type === 'team') {
175
+ selectedTeams.push(item.slug);
176
+ } else if (item.type === 'individual') {
177
+ selectedIndividuals.push(item.login);
178
+ }
179
+ });
180
+
181
+ console.log('');
182
+ if (selectedTeams.length > 0) {
183
+ log.success(`已選擇團隊: ${selectedTeams.join(', ')}`);
184
+ }
185
+ if (selectedIndividuals.length > 0) {
186
+ log.success(`已選擇個人: ${selectedIndividuals.map((u) => `@${u}`).join(', ')}`);
187
+ }
188
+ console.log('');
189
+
190
+ return { teams: selectedTeams, individuals: selectedIndividuals };
191
+ }
192
+
193
+ /**
194
+ * 自動選擇 Reviewers(非互動模式)
195
+ */
196
+ autoSelectReviewers(suggestedReviewers, currentUser) {
197
+ const reviewers = suggestedReviewers
198
+ .filter((r) => {
199
+ // 過濾當前用戶
200
+ if (currentUser && r.email.toLowerCase() === currentUser.email.toLowerCase()) {
201
+ return false;
202
+ }
203
+ // 過濾排除列表中的作者
204
+ const shouldExclude = this.excludeAuthors.some((excluded) => {
205
+ const normalizedExcluded = excluded.toLowerCase();
206
+ return (
207
+ r.email.toLowerCase().includes(normalizedExcluded) ||
208
+ (r.name && r.name.toLowerCase().includes(normalizedExcluded))
209
+ );
210
+ });
211
+ return !shouldExclude;
212
+ })
213
+ .slice(0, CONSTANTS.AUTO_REVIEWERS_COUNT)
214
+ .map((r) => r.email.split('@')[0]);
215
+
216
+ return { teams: [], individuals: reviewers };
217
+ }
218
+
219
+ /**
220
+ * 選擇 Reviewers(根據模式)
221
+ */
222
+ async select(teamsData, suggestedReviewers, currentUser) {
223
+ if (this.interactiveReviewers) {
224
+ // interactiveReviewers: true 表示啟用互動式選擇(手動選擇)
225
+ return this.selectInteractive(teamsData, suggestedReviewers, currentUser);
226
+ } else {
227
+ // interactiveReviewers: false 表示自動選擇(基於 Git 歷史)
228
+ console.log('🤖 自動選擇 reviewers(基於 Git 歷史)\n');
229
+ return this.autoSelectReviewers(suggestedReviewers, currentUser);
230
+ }
231
+ }
232
+ }
@@ -1,31 +1,8 @@
1
- /**
2
- * 互動式選擇工具(使用方向鍵和空白鍵)
3
- * 基於 scripts/ai-pr-modules/ui/interactive-select.mjs
4
- */
5
-
6
1
  import { emitKeypressEvents } from 'readline';
7
-
8
- // ANSI 顏色碼
9
- const colors = {
10
- reset: '\x1b[0m',
11
- bright: '\x1b[1m',
12
- cyan: '\x1b[36m',
13
- green: '\x1b[32m',
14
- yellow: '\x1b[33m',
15
- magenta: '\x1b[35m',
16
- blue: '\x1b[34m',
17
- };
18
-
19
- // 游標控制
20
- const cursor = {
21
- hide: '\x1b[?25l',
22
- show: '\x1b[?25h',
23
- up: (n = 1) => `\x1b[${n}A`,
24
- clearLine: '\x1b[2K',
25
- };
2
+ import { colors, cursor } from '../utils/constants.js';
26
3
 
27
4
  /**
28
- * 互動式選擇器
5
+ * 互動式選擇工具(使用方向鍵和空白鍵)
29
6
  */
30
7
  export class InteractiveSelect {
31
8
  /**
@@ -70,12 +47,12 @@ export class InteractiveSelect {
70
47
  const isCurrent = idx === currentIndex;
71
48
 
72
49
  const checkbox = isSelected ? `${colors.green}[✓]${colors.reset}` : '[ ]';
73
- const cursorMarker = isCurrent ? `${colors.cyan}▶${colors.reset}` : ' ';
50
+ const cursor_marker = isCurrent ? `${colors.cyan}▶${colors.reset}` : ' ';
74
51
  const label = item.label || item.name || item.login;
75
52
  const extra = item.extra || '';
76
53
 
77
54
  process.stdout.write(cursor.clearLine);
78
- console.log(`${cursorMarker} ${checkbox} ${label}${extra}`);
55
+ console.log(`${cursor_marker} ${checkbox} ${label}${extra}`);
79
56
  });
80
57
  };
81
58
 
@@ -0,0 +1,40 @@
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
+ }