ai-git-tools 2.0.6 → 2.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-git-tools",
3
- "version": "2.0.6",
3
+ "version": "2.0.7",
4
4
  "description": "AI-powered Git automation tools for commit messages and PR generation",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -5,12 +5,12 @@
5
5
  */
6
6
 
7
7
  import { execSync } from 'child_process';
8
- import * as readline from 'readline/promises';
9
- import { stdin as input, stdout as output } from 'process';
10
8
  import { loadPRConfig } from '../core/config-loader.js';
11
9
  import { GitOperations } from '../core/git-operations.js';
12
10
  import { AIClient } from '../core/ai-client.js';
11
+ import { GitHubAPI } from '../core/github-api.js';
13
12
  import { Logger } from '../utils/logger.js';
13
+ import { InteractiveSelect } from '../utils/interactive-select.js';
14
14
  import { handleError, getProjectTypePrompt } from '../utils/helpers.js';
15
15
 
16
16
  /**
@@ -78,65 +78,139 @@ function getCurrentUser() {
78
78
  }
79
79
 
80
80
  /**
81
- * 互動式選擇 Reviewers
81
+ * 互動式選擇 Reviewers(使用鍵盤上下選擇)
82
82
  */
83
- async function selectReviewers(suggestedReviewers, currentUser) {
83
+ async function selectReviewers(suggestedReviewers, currentUser, config) {
84
84
  const logger = new Logger();
85
85
 
86
86
  console.log('\n' + '═'.repeat(80));
87
87
  console.log('🎯 選擇 Reviewers');
88
88
  console.log('═'.repeat(80) + '\n');
89
89
 
90
- // 過濾掉當前使用者
91
- const validReviewers = suggestedReviewers.filter(
92
- (r) => !currentUser || r.email.toLowerCase() !== currentUser.email.toLowerCase()
93
- );
94
-
95
- if (validReviewers.length === 0) {
96
- logger.warning('沒有找到建議的 reviewers');
97
- console.log('💡 您可以在創建 PR 後手動添加 reviewers\n');
98
- return [];
90
+ // 先嘗試從 GitHub 抓取團隊和成員
91
+ let githubTeams = {};
92
+ let githubMembers = [];
93
+
94
+ if (config.orgName) {
95
+ const githubAPI = new GitHubAPI({ orgName: config.orgName });
96
+ const githubData = await githubAPI.fetchTeams(config.orgName);
97
+ githubTeams = githubData.teams || {};
98
+ githubMembers = githubData.members || [];
99
99
  }
100
100
 
101
- // 顯示建議的 Reviewers
101
+ // 準備選項列表
102
+ const options = [];
103
+
104
+ // 添加團隊選項
105
+ Object.entries(githubTeams).forEach(([slug, team]) => {
106
+ const memberCount = team.members.length;
107
+ options.push({
108
+ type: 'team',
109
+ slug,
110
+ name: team.name,
111
+ label: `${team.name}`,
112
+ extra: ` (${memberCount} 位成員)`,
113
+ members: team.members,
114
+ });
115
+ });
116
+
117
+ // 添加個人選項(優先使用 Git 歷史建議)
118
+ const individualOptions = new Map();
119
+
120
+ // 先加入建議的 reviewers(基於 Git 歷史)
102
121
  console.log('💡 建議的 Reviewers(基於 Git 歷史):\n');
103
- validReviewers.forEach((reviewer, idx) => {
122
+ let suggestionDisplayed = false;
123
+
124
+ suggestedReviewers.forEach((reviewer) => {
125
+ // 自動排除當前使用者(在顯示時就排除)
126
+ if (currentUser && reviewer.email.toLowerCase() === currentUser.email.toLowerCase()) {
127
+ return; // 跳過當前使用者
128
+ }
129
+
130
+ suggestionDisplayed = true;
104
131
  console.log(
105
- ` ${idx + 1}. ${reviewer.name} (@${reviewer.username}) - ${reviewer.commits} commits`
132
+ ` ${reviewer.name} (@${reviewer.username}) - ${reviewer.commits} commits`
106
133
  );
134
+
135
+ individualOptions.set(reviewer.username, {
136
+ type: 'individual',
137
+ login: reviewer.username,
138
+ name: reviewer.name,
139
+ label: `${reviewer.name} (@${reviewer.username})`,
140
+ extra: ` 💡 ${reviewer.commits} commits`,
141
+ suggested: true,
142
+ });
107
143
  });
108
- console.log('');
109
144
 
110
- const rl = readline.createInterface({ input, output });
145
+ if (suggestionDisplayed) {
146
+ console.log('');
147
+ }
111
148
 
112
- try {
113
- const answer = await rl.question(
114
- '請輸入要選擇的 reviewer 編號(用逗號分隔,例如: 1,2,3),或直接按 Enter 跳過: '
115
- );
116
- rl.close();
149
+ // 再加入其他 GitHub 組織成員
150
+ githubMembers.forEach((member) => {
151
+ if (!individualOptions.has(member.login)) {
152
+ // 同樣自動排除當前使用者
153
+ if (currentUser && member.login.toLowerCase() === currentUser.username?.toLowerCase()) {
154
+ return;
155
+ }
117
156
 
118
- if (!answer.trim()) {
119
- logger.info('已跳過 reviewer 選擇\n');
120
- return [];
157
+ individualOptions.set(member.login, {
158
+ type: 'individual',
159
+ login: member.login,
160
+ name: member.name,
161
+ label: `${member.name} (@${member.login})`,
162
+ extra: '',
163
+ suggested: false,
164
+ });
121
165
  }
166
+ });
122
167
 
123
- const selectedIndices = answer
124
- .split(',')
125
- .map((s) => parseInt(s.trim()) - 1)
126
- .filter((i) => i >= 0 && i < validReviewers.length);
168
+ // 將個人選項加入列表(建議的排在前面)
169
+ const sortedIndividuals = Array.from(individualOptions.values()).sort((a, b) => {
170
+ if (a.suggested && !b.suggested) return -1;
171
+ if (!a.suggested && b.suggested) return 1;
172
+ return 0;
173
+ });
127
174
 
128
- const selectedReviewers = selectedIndices.map((i) => validReviewers[i].username);
175
+ options.push(...sortedIndividuals);
129
176
 
130
- if (selectedReviewers.length > 0) {
131
- logger.success(`已選擇: ${selectedReviewers.map((u) => `@${u}`).join(', ')}\n`);
132
- }
177
+ // 如果完全沒有選項
178
+ if (options.length === 0) {
179
+ logger.warning('沒有找到可用的 reviewers');
180
+ console.log('💡 您可以在創建 PR 後手動添加 reviewers\n');
181
+ return [];
182
+ }
133
183
 
134
- return selectedReviewers;
135
- } catch (error) {
136
- rl.close();
137
- logger.warning('選擇過程發生錯誤,將跳過 reviewer 選擇\n');
184
+ // 使用互動式選擇器
185
+ const selector = new InteractiveSelect();
186
+ const result = await selector.select(options, '🎯 選擇 Reviewers (可多選)');
187
+
188
+ if (result.cancelled) {
189
+ logger.info('已跳過 reviewer 選擇\n');
138
190
  return [];
139
191
  }
192
+
193
+ // 處理選擇結果
194
+ const selectedReviewers = [];
195
+ const selectedTeams = [];
196
+
197
+ result.selected.forEach((item) => {
198
+ if (item.type === 'team') {
199
+ selectedTeams.push(item.slug);
200
+ } else {
201
+ selectedReviewers.push(item.login);
202
+ }
203
+ });
204
+
205
+ if (selectedReviewers.length > 0 || selectedTeams.length > 0) {
206
+ const reviewerList = selectedReviewers.map((u) => `@${u}`).join(', ');
207
+ const teamList = selectedTeams.map((t) => `@${config.orgName}/${t}`).join(', ');
208
+ const allSelected = [reviewerList, teamList].filter(Boolean).join(', ');
209
+
210
+ logger.success(`已選擇: ${allSelected}\n`);
211
+ }
212
+
213
+ return { individuals: selectedReviewers, teams: selectedTeams };
140
214
  }
141
215
 
142
216
  /**
@@ -624,13 +698,13 @@ export async function prCommand() {
624
698
  }
625
699
 
626
700
  // 5. Reviewer 選擇(如果啟用)
627
- let selectedReviewers = [];
701
+ let selectedReviewers = { individuals: [], teams: [] };
628
702
  if (config.reviewers.autoSelect) {
629
703
  const currentUser = getCurrentUser();
630
704
  const suggestedReviewers = getReviewersByGitHistory(changedFiles, config);
631
705
 
632
- if (suggestedReviewers.length > 0) {
633
- selectedReviewers = await selectReviewers(suggestedReviewers, currentUser);
706
+ if (suggestedReviewers.length > 0 || config.orgName) {
707
+ selectedReviewers = await selectReviewers(suggestedReviewers, currentUser, config);
634
708
  } else {
635
709
  logger.info('未找到建議的 reviewers(可能是新專案或檔案無歷史記錄)\n');
636
710
  }
@@ -655,9 +729,19 @@ export async function prCommand() {
655
729
  ghCommand += ' --draft';
656
730
  }
657
731
 
658
- // 添加 reviewers
659
- if (selectedReviewers.length > 0) {
660
- ghCommand += ` --reviewer ${selectedReviewers.join(',')}`;
732
+ // 添加 individual reviewers
733
+ if (selectedReviewers.individuals && selectedReviewers.individuals.length > 0) {
734
+ ghCommand += ` --reviewer ${selectedReviewers.individuals.join(',')}`;
735
+ }
736
+
737
+ // 添加 team reviewers(需要加上組織名稱前綴)
738
+ if (selectedReviewers.teams && selectedReviewers.teams.length > 0) {
739
+ const teamReviewers = selectedReviewers.teams.map((team) => `${config.orgName}/${team}`);
740
+ if (selectedReviewers.individuals && selectedReviewers.individuals.length > 0) {
741
+ ghCommand += `,${teamReviewers.join(',')}`;
742
+ } else {
743
+ ghCommand += ` --reviewer ${teamReviewers.join(',')}`;
744
+ }
661
745
  }
662
746
 
663
747
  try {
@@ -0,0 +1,161 @@
1
+ /**
2
+ * GitHub API 操作封裝
3
+ * 基於 scripts/ai-pr-modules/core/github-api.mjs
4
+ */
5
+
6
+ import { execSync } from 'child_process';
7
+ import { Logger } from '../utils/logger.js';
8
+
9
+ /**
10
+ * GitHub API 操作類
11
+ */
12
+ export class GitHubAPI {
13
+ constructor(config = {}) {
14
+ this.orgName = config.orgName || 'kingsinfo-project';
15
+ }
16
+
17
+ /**
18
+ * 檢查 GitHub CLI 認證狀態
19
+ */
20
+ checkAuth() {
21
+ try {
22
+ const authStatus = execSync('gh auth status 2>&1', {
23
+ encoding: 'utf-8',
24
+ stdio: ['pipe', 'pipe', 'pipe'],
25
+ });
26
+
27
+ return {
28
+ authenticated: authStatus.includes('Logged in'),
29
+ details: authStatus,
30
+ };
31
+ } catch (error) {
32
+ return {
33
+ authenticated: false,
34
+ details: error.message,
35
+ };
36
+ }
37
+ }
38
+
39
+ /**
40
+ * 從 GitHub 抓取組織的成員列表
41
+ */
42
+ async fetchOrgMembers(orgName = this.orgName) {
43
+ const logger = new Logger();
44
+
45
+ try {
46
+ logger.step(`正在嘗試抓取 ${orgName} 組織的成員列表...`);
47
+
48
+ const membersJson = execSync(`gh api orgs/${orgName}/members --jq '.'`, {
49
+ encoding: 'utf-8',
50
+ stdio: ['pipe', 'pipe', 'pipe'],
51
+ });
52
+
53
+ const membersData = JSON.parse(membersJson);
54
+ const members = membersData.map((member) => ({
55
+ login: member.login,
56
+ name: member.name || member.login,
57
+ }));
58
+
59
+ if (members.length > 0) {
60
+ logger.success(`成功抓取 ${members.length} 位組織成員\n`);
61
+ return members;
62
+ }
63
+
64
+ return [];
65
+ } catch (error) {
66
+ return [];
67
+ }
68
+ }
69
+
70
+ /**
71
+ * 從 GitHub 抓取組織的團隊列表
72
+ */
73
+ async fetchTeams(orgName = this.orgName) {
74
+ const logger = new Logger();
75
+
76
+ try {
77
+ // 先檢查認證狀態
78
+ const authStatus = this.checkAuth();
79
+ if (!authStatus.authenticated) {
80
+ logger.warning('GitHub CLI 未認證');
81
+ logger.info('請執行: gh auth login\n');
82
+ return { teams: {}, members: [] };
83
+ }
84
+
85
+ logger.step(`正在從 GitHub 抓取 ${orgName} 的團隊資訊...`);
86
+
87
+ let teamsJson;
88
+ try {
89
+ teamsJson = execSync(`gh api orgs/${orgName}/teams --jq '.'`, {
90
+ encoding: 'utf-8',
91
+ stdio: ['pipe', 'pipe', 'pipe'],
92
+ });
93
+ } catch (error) {
94
+ // 無法存取團隊 API,使用替代方案
95
+ logger.warning('未找到任何團隊,嘗試直接抓取組織成員...');
96
+
97
+ const members = await this.fetchOrgMembers(orgName);
98
+ if (members.length > 0) {
99
+ return { teams: {}, members };
100
+ }
101
+
102
+ throw error;
103
+ }
104
+
105
+ const teams = JSON.parse(teamsJson);
106
+
107
+ if (!teams || teams.length === 0) {
108
+ logger.warning('未找到任何團隊,嘗試直接抓取組織成員...');
109
+ const members = await this.fetchOrgMembers(orgName);
110
+ return { teams: {}, members };
111
+ }
112
+
113
+ const teamData = {};
114
+
115
+ for (const team of teams) {
116
+ try {
117
+ const membersJson = execSync(
118
+ `gh api orgs/${orgName}/teams/${team.slug}/members --jq '.'`,
119
+ {
120
+ encoding: 'utf-8',
121
+ stdio: ['pipe', 'pipe', 'pipe'],
122
+ }
123
+ );
124
+
125
+ const members = JSON.parse(membersJson);
126
+ teamData[team.slug] = {
127
+ name: team.name,
128
+ slug: team.slug,
129
+ description: team.description || '',
130
+ members: members.map((m) => ({
131
+ login: m.login,
132
+ name: m.name || m.login,
133
+ })),
134
+ };
135
+ } catch (error) {
136
+ logger.warning(`無法抓取團隊 ${team.slug} 的成員: ${error.message}`);
137
+ }
138
+ }
139
+
140
+ // 同時取得所有成員作為備選
141
+ const allMembers = await this.fetchOrgMembers(orgName);
142
+
143
+ logger.success(
144
+ `成功抓取 ${Object.keys(teamData).length} 個團隊和 ${allMembers.length} 位成員\n`
145
+ );
146
+ return { teams: teamData, members: allMembers };
147
+ } catch (error) {
148
+ logger.warning('無法從 GitHub 抓取資訊');
149
+ logger.info('請確認:');
150
+ console.log(' 1. 已安裝 GitHub CLI: brew install gh');
151
+ console.log(' 2. 已執行認證: gh auth login');
152
+ console.log(' 3. 選擇正確的認證範圍(需要 read:org 權限)');
153
+ console.log(` 4. 有權限存取 ${orgName} 組織`);
154
+ console.log(' 5. 組織名稱正確(可用 --org 參數指定)\n');
155
+
156
+ logger.info('提示: 你仍可以在創建 PR 後手動添加 reviewers\n');
157
+
158
+ return { teams: {}, members: [] };
159
+ }
160
+ }
161
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * 互動式選擇工具(使用方向鍵和空白鍵)
3
+ * 基於 scripts/ai-pr-modules/ui/interactive-select.mjs
4
+ */
5
+
6
+ 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
+ };
26
+
27
+ /**
28
+ * 互動式選擇器
29
+ */
30
+ export class InteractiveSelect {
31
+ /**
32
+ * @param {Array} options - 選項陣列
33
+ * @param {string} title - 標題
34
+ * @returns {Promise<{cancelled: boolean, selected: Array}>}
35
+ */
36
+ async select(options, title = '選擇項目') {
37
+ return new Promise((resolve) => {
38
+ let currentIndex = 0;
39
+ const selected = new Set();
40
+
41
+ // 準備選項列表
42
+ const items = options.map((opt, idx) => ({
43
+ ...opt,
44
+ index: idx,
45
+ }));
46
+
47
+ const render = () => {
48
+ // 清除整個選擇區域
49
+ if (items.length > 0) {
50
+ // 向上移動到開始位置
51
+ for (let i = 0; i < items.length + 3; i++) {
52
+ process.stdout.write(cursor.up(1));
53
+ }
54
+ }
55
+
56
+ // 清除並重新繪製每一行
57
+ process.stdout.write(cursor.clearLine);
58
+ console.log(`${colors.bright}${title}${colors.reset}`);
59
+
60
+ process.stdout.write(cursor.clearLine);
61
+ console.log(
62
+ `${colors.yellow}↑/↓: 移動 Space: 選擇/取消 Enter: 確認 q: 跳過${colors.reset}`
63
+ );
64
+
65
+ process.stdout.write(cursor.clearLine);
66
+ console.log('');
67
+
68
+ items.forEach((item, idx) => {
69
+ const isSelected = selected.has(idx);
70
+ const isCurrent = idx === currentIndex;
71
+
72
+ const checkbox = isSelected ? `${colors.green}[✓]${colors.reset}` : '[ ]';
73
+ const cursorMarker = isCurrent ? `${colors.cyan}▶${colors.reset}` : ' ';
74
+ const label = item.label || item.name || item.login;
75
+ const extra = item.extra || '';
76
+
77
+ process.stdout.write(cursor.clearLine);
78
+ console.log(`${cursorMarker} ${checkbox} ${label}${extra}`);
79
+ });
80
+ };
81
+
82
+ const cleanup = () => {
83
+ process.stdin.setRawMode(false);
84
+ process.stdin.removeAllListeners('keypress');
85
+ process.stdout.write(cursor.show);
86
+ console.log('');
87
+ };
88
+
89
+ const handleKeypress = (str, key) => {
90
+ if (!key) return;
91
+
92
+ // q or Ctrl+C to quit
93
+ if (key.name === 'q' || (key.ctrl && key.name === 'c')) {
94
+ cleanup();
95
+ resolve({ cancelled: true, selected: [] });
96
+ return;
97
+ }
98
+
99
+ // Arrow up
100
+ if (key.name === 'up') {
101
+ currentIndex = Math.max(0, currentIndex - 1);
102
+ render();
103
+ }
104
+
105
+ // Arrow down
106
+ else if (key.name === 'down') {
107
+ currentIndex = Math.min(items.length - 1, currentIndex + 1);
108
+ render();
109
+ }
110
+
111
+ // Space to toggle selection
112
+ else if (key.name === 'space') {
113
+ if (selected.has(currentIndex)) {
114
+ selected.delete(currentIndex);
115
+ } else {
116
+ selected.add(currentIndex);
117
+ }
118
+ render();
119
+ }
120
+
121
+ // Enter to confirm
122
+ else if (key.name === 'return') {
123
+ cleanup();
124
+ const selectedItems = Array.from(selected).map((idx) => items[idx]);
125
+ resolve({ cancelled: false, selected: selectedItems });
126
+ }
127
+ };
128
+
129
+ // 初始化
130
+ process.stdout.write(cursor.hide);
131
+
132
+ // 先印出佔位用的空行
133
+ for (let i = 0; i < items.length + 3; i++) {
134
+ console.log('');
135
+ }
136
+
137
+ render();
138
+
139
+ // 啟用 raw mode 和 keypress
140
+ if (process.stdin.isTTY) {
141
+ process.stdin.setRawMode(true);
142
+ process.stdin.resume();
143
+
144
+ // 需要監聽 keypress 事件
145
+ emitKeypressEvents(process.stdin);
146
+
147
+ process.stdin.on('keypress', handleKeypress);
148
+ } else {
149
+ cleanup();
150
+ resolve({ cancelled: true, selected: [] });
151
+ }
152
+ });
153
+ }
154
+ }