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.
@@ -6,6 +6,26 @@
6
6
  import { CopilotClient, approveAll } from '@github/copilot-sdk';
7
7
 
8
8
  export class AIClient {
9
+ /**
10
+ * 檢測是否為 Copilot 授權相關的錯誤
11
+ */
12
+ static isCopilotAuthError(error) {
13
+ const message = error?.message || error?.toString() || '';
14
+ const lowerMessage = message.toLowerCase();
15
+
16
+ return (
17
+ lowerMessage.includes('permission') ||
18
+ lowerMessage.includes('unauthorized') ||
19
+ lowerMessage.includes('forbidden') ||
20
+ lowerMessage.includes('not authorized') ||
21
+ lowerMessage.includes('authentication failed') ||
22
+ lowerMessage.includes('access denied') ||
23
+ lowerMessage.includes('you do not have access') ||
24
+ lowerMessage.includes('copilot') ||
25
+ message.includes('EACCES')
26
+ );
27
+ }
28
+
9
29
  /**
10
30
  * 發送 prompt 並等待回應(帶重試機制和超時保護)
11
31
  */
@@ -15,11 +35,11 @@ export class AIClient {
15
35
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
16
36
  const client = new CopilotClient();
17
37
  try {
18
- const session = await client.createSession({
38
+ const session = await client.createSession({
19
39
  model,
20
- onPermissionRequest: approveAll
40
+ onPermissionRequest: approveAll,
21
41
  });
22
-
42
+
23
43
  // 使用 Promise.race 實現超時控制
24
44
  const responsePromise = session.sendAndWait({ prompt });
25
45
  const timeoutPromise = new Promise((_, reject) => {
@@ -34,7 +54,7 @@ export class AIClient {
34
54
  lastError = error;
35
55
  if (attempt < maxRetries) {
36
56
  console.log(`⚠️ AI 請求失敗,重試第 ${attempt}/${maxRetries} 次...`);
37
- await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
57
+ await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
38
58
  }
39
59
  } finally {
40
60
  // 確保每次都關閉 client,無論成功或失敗
@@ -46,7 +66,11 @@ export class AIClient {
46
66
  }
47
67
  }
48
68
 
49
- throw new Error(`AI 請求失敗: ${lastError?.message || '未知錯誤'}`);
69
+ // 區分不同類型的錯誤
70
+ const errorObj = new Error(`AI 請求失敗: ${lastError?.message || '未知錯誤'}`);
71
+ errorObj.originalError = lastError;
72
+ errorObj.isCopilotAuth = AIClient.isCopilotAuthError(lastError);
73
+ throw errorObj;
50
74
  }
51
75
 
52
76
  /**
@@ -54,7 +78,10 @@ export class AIClient {
54
78
  */
55
79
  static parseJSON(content) {
56
80
  // 移除可能的 markdown code block 標記
57
- const jsonContent = content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
81
+ const jsonContent = content
82
+ .replace(/```json\n?/g, '')
83
+ .replace(/```\n?/g, '')
84
+ .trim();
58
85
 
59
86
  try {
60
87
  return JSON.parse(jsonContent);
@@ -1,12 +1,38 @@
1
1
  /**
2
2
  * 配置載入器
3
- * 基於 scripts/commit-modules/config-loader.mjs scripts/ai-pr-modules/core/config-loader.mjs
4
- * 支援從任何目錄下的 .ai-git-config.mjs 載入配置
3
+ * 統一 commit PR 命令的配置載入
4
+ * 支援從目前工作目錄下的 .ai-git-config.mjs 載入配置
5
5
  */
6
6
 
7
7
  import { existsSync } from 'fs';
8
8
  import { resolve } from 'path';
9
9
 
10
+ /**
11
+ * 內建預設配置
12
+ */
13
+ const DEFAULT_CONFIG = {
14
+ ai: {
15
+ model: 'gpt-4.1',
16
+ maxDiffLength: 8000,
17
+ maxRetries: 3,
18
+ },
19
+ github: {
20
+ defaultBase: 'release',
21
+ autoLabels: true,
22
+ includeImpactAnalysis: false,
23
+ },
24
+ reviewers: {
25
+ interactiveReviewers: true,
26
+ maxSuggested: 5,
27
+ gitHistoryDepth: 20,
28
+ excludeAuthors: [],
29
+ },
30
+ output: {
31
+ verbose: false,
32
+ saveHistory: false,
33
+ },
34
+ };
35
+
10
36
  /**
11
37
  * 解析命令行參數
12
38
  */
@@ -17,6 +43,13 @@ export function parseCliArgs() {
17
43
  verbose: false,
18
44
  maxDiffLength: null,
19
45
  maxRetries: null,
46
+ baseBranch: null,
47
+ headBranch: null,
48
+ preview: false,
49
+ noConfirm: false,
50
+ interactiveReviewers: undefined,
51
+ autoLabels: null,
52
+ includeImpactAnalysis: null,
20
53
  };
21
54
 
22
55
  for (let i = 0; i < args.length; i++) {
@@ -34,91 +67,11 @@ export function parseCliArgs() {
34
67
  case '--max-retries':
35
68
  config.maxRetries = parseInt(args[++i], 10);
36
69
  break;
37
- default:
38
- break;
39
- }
40
- }
41
-
42
- return config;
43
- }
44
-
45
- /**
46
- * 載入配置(commit 工具使用)
47
- * 支援從目前工作目錄或使用者專案目錄載入 .ai-git-config.mjs
48
- */
49
- export async function loadCommitConfig() {
50
- // 內建預設值
51
- const defaults = {
52
- ai: {
53
- model: 'gpt-4.1',
54
- maxDiffLength: 8000,
55
- maxRetries: 3,
56
- },
57
- output: {
58
- verbose: false,
59
- saveHistory: false,
60
- },
61
- };
62
-
63
- // 嘗試從目前工作目錄載入配置檔案
64
- const configPath = resolve(process.cwd(), '.ai-git-config.mjs');
65
- let userConfig = {};
66
-
67
- if (existsSync(configPath)) {
68
- try {
69
- const imported = await import(`file://${configPath}`);
70
- userConfig = imported.default || {};
71
- } catch (error) {
72
- console.warn(`⚠️ 載入配置檔案失敗: ${error.message}`);
73
- }
74
- }
75
-
76
- // 合併配置
77
- const config = {
78
- ai: {
79
- model: userConfig.ai?.model ?? defaults.ai.model,
80
- maxDiffLength: userConfig.ai?.maxDiffLength ?? defaults.ai.maxDiffLength,
81
- maxRetries: userConfig.ai?.maxRetries ?? defaults.ai.maxRetries,
82
- },
83
- output: {
84
- verbose: userConfig.output?.verbose ?? defaults.output.verbose,
85
- saveHistory: userConfig.output?.saveHistory ?? defaults.output.saveHistory,
86
- },
87
- };
88
-
89
- // 命令行參數優先
90
- const cliConfig = parseCliArgs();
91
- if (cliConfig.model) config.ai.model = cliConfig.model;
92
- if (cliConfig.verbose) config.output.verbose = cliConfig.verbose;
93
- if (cliConfig.maxDiffLength) config.ai.maxDiffLength = cliConfig.maxDiffLength;
94
- if (cliConfig.maxRetries) config.ai.maxRetries = cliConfig.maxRetries;
95
-
96
- return config;
97
- }
98
-
99
- /**
100
- * 解析 PR 命令行參數
101
- */
102
- export function parsePRCliArgs() {
103
- const args = process.argv.slice(2);
104
- const config = {
105
- baseBranch: null,
106
- headBranch: null,
107
- model: null,
108
- draft: false,
109
- preview: false,
110
- noConfirm: false,
111
- autoReviewers: false,
112
- autoLabels: null,
113
- };
114
-
115
- for (let i = 0; i < args.length; i++) {
116
- switch (args[i]) {
117
70
  case '--base':
118
71
  config.baseBranch = args[++i];
119
72
  break;
120
- case '--model':
121
- config.model = args[++i];
73
+ case '--head':
74
+ config.headBranch = args[++i];
122
75
  break;
123
76
  case '--preview':
124
77
  config.preview = true;
@@ -126,9 +79,19 @@ export function parsePRCliArgs() {
126
79
  case '--no-confirm':
127
80
  config.noConfirm = true;
128
81
  break;
82
+ case '--interactive-reviewers':
83
+ config.interactiveReviewers = true;
84
+ break;
129
85
  case '--auto-labels':
130
86
  config.autoLabels = true;
131
87
  break;
88
+ case '--include-impact':
89
+ config.includeImpactAnalysis = true;
90
+ break;
91
+ case '--help':
92
+ showHelp();
93
+ process.exit(0);
94
+ break;
132
95
  default:
133
96
  break;
134
97
  }
@@ -138,44 +101,119 @@ export function parsePRCliArgs() {
138
101
  }
139
102
 
140
103
  /**
141
- * 載入 PR 配置
104
+ * 深度合併物件(簡易版)
142
105
  */
143
- export async function loadPRConfig() {
144
- // 嘗試從目前工作目錄載入配置檔案
145
- const configPath = resolve(process.cwd(), '.ai-git-config.mjs');
146
- let config = null;
147
-
148
- if (existsSync(configPath)) {
149
- try {
150
- const userConfig = await import(`file://${configPath}`);
151
- config = userConfig.default;
152
- } catch (error) {
153
- console.warn(`⚠️ 載入配置檔案失敗: ${error.message}`);
106
+ function mergeDeep(target, source) {
107
+ const result = { ...target };
108
+ for (const key of Object.keys(source)) {
109
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
110
+ result[key] = mergeDeep(result[key] || {}, source[key]);
111
+ } else {
112
+ result[key] = source[key];
154
113
  }
155
114
  }
115
+ return result;
116
+ }
117
+
118
+ /**
119
+ * 載入使用者配置檔案
120
+ */
121
+ export async function loadUserConfig() {
122
+ const configPath = resolve(process.cwd(), '.ai-git-config.mjs');
156
123
 
157
- // 使用預設配置
158
- if (!config) {
159
- config = {
160
- ai: { model: 'gpt-4.1', maxDiffLength: 8000, maxRetries: 3 },
161
- github: { defaultBase: 'release', autoLabels: true },
162
- reviewers: { autoSelect: true, maxSuggested: 5, gitHistoryDepth: 20, excludeAuthors: [] },
163
- output: { verbose: false, saveHistory: false },
164
- };
124
+ if (!existsSync(configPath)) {
125
+ return DEFAULT_CONFIG;
165
126
  }
166
127
 
167
- // 確保 github 物件存在
168
- if (!config.github) {
169
- config.github = {};
128
+ try {
129
+ const imported = await import(`file://${configPath}`);
130
+ const userConfig = imported.default || {};
131
+ return mergeDeep(DEFAULT_CONFIG, userConfig);
132
+ } catch (error) {
133
+ console.warn(`⚠️ 載入配置檔案失敗: ${error.message}`);
134
+ return DEFAULT_CONFIG;
170
135
  }
136
+ }
137
+
138
+ /**
139
+ * 載入統一配置(commit 與 PR 共用)
140
+ * 已廢棄:保留給舊 import 路徑向後相容,請改用 loadConfig
141
+ */
142
+ export async function loadCommitConfig() {
143
+ return loadConfig();
144
+ }
145
+
146
+ /**
147
+ * 載入統一配置(PR 使用)
148
+ */
149
+ export async function loadPRConfig() {
150
+ return loadConfig();
151
+ }
152
+
153
+ /**
154
+ * 載入統一配置
155
+ */
156
+ export async function loadConfig() {
157
+ const config = await loadUserConfig();
158
+ const cliConfig = parseCliArgs();
159
+
160
+ // 確保各區塊存在
161
+ config.ai = config.ai || {};
162
+ config.github = config.github || {};
163
+ config.reviewers = config.reviewers || {};
164
+ config.output = config.output || {};
171
165
 
172
- // 合併命令行參數
173
- const cliConfig = parsePRCliArgs();
166
+ // 合併命令行參數(CLI 優先)
174
167
  if (cliConfig.model) config.ai.model = cliConfig.model;
175
- config.headBranch = cliConfig.headBranch;
176
- config.draft = cliConfig.draft;
177
- config.preview = cliConfig.preview;
178
- config.noConfirm = cliConfig.noConfirm;
168
+ if (cliConfig.verbose) config.output.verbose = cliConfig.verbose;
169
+ if (cliConfig.maxDiffLength) config.ai.maxDiffLength = cliConfig.maxDiffLength;
170
+ if (cliConfig.maxRetries) config.ai.maxRetries = cliConfig.maxRetries;
171
+ if (cliConfig.baseBranch) config.baseBranch = cliConfig.baseBranch;
172
+ if (cliConfig.headBranch) config.headBranch = cliConfig.headBranch;
173
+ if (cliConfig.preview) config.preview = cliConfig.preview;
174
+ if (cliConfig.noConfirm) config.noConfirm = cliConfig.noConfirm;
175
+ if (cliConfig.interactiveReviewers !== undefined) {
176
+ config.reviewers.interactiveReviewers = cliConfig.interactiveReviewers;
177
+ }
178
+ if (cliConfig.autoLabels !== null) config.github.autoLabels = cliConfig.autoLabels;
179
+ if (cliConfig.includeImpactAnalysis !== null) {
180
+ config.github.includeImpactAnalysis = cliConfig.includeImpactAnalysis;
181
+ }
179
182
 
180
183
  return config;
181
184
  }
185
+
186
+ /**
187
+ * 顯示 PR 幫助訊息
188
+ */
189
+ function showHelp() {
190
+ console.log(`
191
+ 使用方式:
192
+ npx ai-git-tools pr [選項]
193
+
194
+ 選項:
195
+ --base <branch> 指定目標分支 (預設: 使用配置檔的 defaultBase 或自動偵測)
196
+ --model <model> 指定 AI 模型 (預設: gpt-4.1)
197
+ --preview 僅預覽 PR 內容,不實際創建
198
+ --no-confirm 跳過確認直接創建
199
+ --auto-labels 自動添加 Labels (預設啟用)
200
+ --help 顯示此說明
201
+
202
+ 範例:
203
+ npx ai-git-tools pr
204
+ npx ai-git-tools pr --base release-2025-m12.1
205
+ npx ai-git-tools pr --preview
206
+ npx ai-git-tools pr --no-confirm
207
+
208
+ 配置檔範例 (.ai-git-config.mjs):
209
+ export default {
210
+ github: {
211
+ defaultBase: 'release-2025-m12.1',
212
+ autoLabels: true,
213
+ },
214
+ reviewers: {
215
+ interactiveReviewers: true,
216
+ },
217
+ };
218
+ `);
219
+ }
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * Git 操作封裝
3
- * 基於 scripts/ai-pr-modules/core/git-operations.mjs
3
+ * 統一 commit 與 PR 命令的 Git 操作
4
4
  */
5
5
 
6
6
  import { execSync } from 'child_process';
7
+ import { CONSTANTS } from '../utils/constants.js';
8
+ import { PRError } from '../utils/helpers.js';
7
9
 
8
- const MAX_BUFFER_SIZE = 10 * 1024 * 1024; // 10MB
9
- const DIFF_CONTEXT_LINES = 50;
10
+ const MAX_BUFFER_SIZE = CONSTANTS.MAX_BUFFER_SIZE;
11
+ const DIFF_CONTEXT_LINES = CONSTANTS.DIFF_CONTEXT_LINES;
10
12
 
11
13
  export class GitOperations {
12
14
  /**
@@ -60,7 +62,11 @@ export class GitOperations {
60
62
  */
61
63
  static detectReleaseBranches() {
62
64
  try {
63
- execSync('git fetch origin', { stdio: 'ignore' });
65
+ try {
66
+ execSync('git fetch --prune origin', { stdio: 'ignore', timeout: 15000 });
67
+ } catch (_) {
68
+ // fetch 失敗,繼續使用已經 cache 的遠端分支
69
+ }
64
70
  const branches = execSync('git branch -r', { encoding: 'utf-8' })
65
71
  .toString()
66
72
  .split('\n')
@@ -82,18 +88,24 @@ export class GitOperations {
82
88
 
83
89
  const monthlyBranches = branches.filter((b) => b.includes('-m'));
84
90
  const weeklyBranches = branches.filter((b) => b.includes('-w'));
85
- const priorityBranches = monthlyBranches.length > 0 ? monthlyBranches : weeklyBranches;
91
+ const priorityBranches =
92
+ monthlyBranches.length > 0
93
+ ? monthlyBranches
94
+ : weeklyBranches.length > 0
95
+ ? weeklyBranches
96
+ : branches;
86
97
 
87
98
  priorityBranches.sort().reverse();
88
- return priorityBranches[0];
99
+ return priorityBranches[0] || null;
89
100
  }
90
101
 
91
102
  /**
92
103
  * 獲取變更的檔案列表
93
104
  */
94
- static getChangedFiles(baseBranch, headBranch) {
105
+ static getChangedFiles(baseBranch, headBranch, useRemoteHead = false) {
95
106
  try {
96
- const files = execSync(`git diff --name-only origin/${baseBranch}...${headBranch}`, {
107
+ const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
108
+ const files = execSync(`git diff --name-only origin/${baseBranch}...${headRef}`, {
97
109
  encoding: 'utf-8',
98
110
  })
99
111
  .split('\n')
@@ -107,35 +119,41 @@ export class GitOperations {
107
119
  /**
108
120
  * 獲取 commit 列表
109
121
  */
110
- static getCommits(baseBranch, headBranch) {
122
+ static getCommits(baseBranch, headBranch, options = {}) {
123
+ const { oneline = true, noDecorate = true, useRemoteHead = false } = options;
111
124
  try {
112
- return execSync(`git log origin/${baseBranch}..origin/${headBranch} --oneline`, {
113
- encoding: 'utf-8',
114
- });
125
+ const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
126
+ let cmd = `git log origin/${baseBranch}..${headRef}`;
127
+ if (oneline) cmd += ' --oneline';
128
+ if (noDecorate) cmd += ' --no-decorate';
129
+
130
+ return execSync(cmd, { encoding: 'utf-8' });
115
131
  } catch (error) {
116
- throw new Error(`無法比較分支差異: ${error.message}`);
132
+ throw new PRError(
133
+ '無法比較分支差異',
134
+ 'GIT_COMPARE_FAILED',
135
+ ['檢查遠端分支是否存在: git branch -r', '執行診斷: npm run diagnose:pr'],
136
+ `git log origin/${baseBranch}..${headBranch}`
137
+ );
117
138
  }
118
139
  }
119
140
 
120
141
  /**
121
142
  * 獲取 diff
122
143
  */
123
- static getDiff(baseBranch, headBranch) {
144
+ static getDiff(baseBranch, headBranch, useRemoteHead = false, maxBuffer = MAX_BUFFER_SIZE) {
124
145
  try {
125
- return execSync(`git diff origin/${baseBranch}...${headBranch}`, {
146
+ const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
147
+ return execSync(`git diff origin/${baseBranch}...${headRef}`, {
126
148
  encoding: 'utf-8',
127
- maxBuffer: MAX_BUFFER_SIZE,
149
+ maxBuffer,
128
150
  });
129
151
  } catch (error) {
130
- // 嘗試替代方案
131
- try {
132
- return execSync(`git diff origin/${baseBranch}..${headBranch}`, {
133
- encoding: 'utf-8',
134
- maxBuffer: MAX_BUFFER_SIZE,
135
- });
136
- } catch (fallbackError) {
137
- throw new Error(`無法獲取分支差異: ${error.message}`);
138
- }
152
+ const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
153
+ return execSync(`git diff origin/${baseBranch}..${headRef}`, {
154
+ encoding: 'utf-8',
155
+ maxBuffer,
156
+ });
139
157
  }
140
158
  }
141
159
 
@@ -143,39 +161,60 @@ export class GitOperations {
143
161
  * 智能截斷 diff
144
162
  * 保留前後各 50 行,中間用省略標記
145
163
  */
146
- static truncateDiff(diff, maxLength = 8000) {
164
+ static truncateDiff(diff, maxLength = CONSTANTS.MAX_DIFF_LENGTH) {
147
165
  if (diff.length <= maxLength) return diff;
148
166
 
149
167
  const lines = diff.split('\n');
150
- const contextLines = DIFF_CONTEXT_LINES;
151
168
 
152
- if (lines.length <= contextLines * 2) {
169
+ if (lines.length <= DIFF_CONTEXT_LINES * 2) {
153
170
  return diff;
154
171
  }
155
172
 
156
- const header = lines.slice(0, contextLines).join('\n');
157
- const footer = lines.slice(-contextLines).join('\n');
158
- const omittedLines = lines.length - contextLines * 2;
173
+ const header = lines.slice(0, DIFF_CONTEXT_LINES).join('\n');
174
+ const footer = lines.slice(-DIFF_CONTEXT_LINES).join('\n');
159
175
 
160
- return `${header}\n\n... [已省略 ${omittedLines} 行變更] ...\n\n${footer}`;
176
+ return `${header}\n\n... [已省略 ${lines.length - DIFF_CONTEXT_LINES * 2} 行變更] ...\n\n${footer}`;
161
177
  }
162
178
 
163
179
  /**
164
180
  * 推送到遠端
165
181
  */
166
- static push(branch) {
182
+ static async push(branch) {
167
183
  try {
168
- execSync(`git push -u origin ${branch}`, { stdio: 'inherit' });
184
+ execSync(`git push -u origin ${branch}`, {
185
+ stdio: ['ignore', 'inherit', 'pipe'],
186
+ encoding: 'utf-8',
187
+ });
169
188
  return true;
170
189
  } catch (error) {
171
- throw new Error(`推送失敗: ${error.message}`);
190
+ const errMsg = (error.stderr || error.message || '').toString();
191
+ const is403 = errMsg.includes('403') || errMsg.includes('Write access') || errMsg.includes('write access');
192
+ if (is403) {
193
+ throw new PRError(
194
+ '推送失敗:git 沒有寫入權限',
195
+ 'GIT_PUSH_FORBIDDEN',
196
+ [
197
+ '建議執行以下指令讓 git 使用 gh 的認證:',
198
+ ' gh auth setup-git',
199
+ '或者改用 SSH 權限:',
200
+ ' git remote set-url origin git@github.com:<org>/<repo>.git',
201
+ ],
202
+ `git push -u origin ${branch}`
203
+ );
204
+ }
205
+ throw new PRError(
206
+ '推送失敗',
207
+ 'GIT_PUSH_FAILED',
208
+ ['檢查是否有推送權限', '檢查遠端分支是否有衝突', '檢查網路連接'],
209
+ `git push -u origin ${branch}`
210
+ );
172
211
  }
173
212
  }
174
213
 
175
214
  /**
176
215
  * 同步遠端資訊
177
216
  */
178
- static fetch() {
217
+ static async fetch() {
179
218
  try {
180
219
  execSync('git fetch origin', { stdio: 'ignore' });
181
220
  return true;
@@ -187,14 +226,15 @@ export class GitOperations {
187
226
  /**
188
227
  * 獲取變更統計
189
228
  */
190
- static getChangeStats(baseBranch, headBranch) {
229
+ static getChangeStats(baseBranch, headBranch, useRemoteHead = false) {
191
230
  try {
192
- const stats = execSync(`git diff --shortstat origin/${baseBranch}...${headBranch}`, {
231
+ const headRef = useRemoteHead ? `origin/${headBranch}` : headBranch;
232
+ const stats = execSync(`git diff --shortstat origin/${baseBranch}...${headRef}`, {
193
233
  encoding: 'utf-8',
194
234
  }).trim();
195
235
 
196
236
  const filesChanged = execSync(
197
- `git diff --name-only origin/${baseBranch}...${headBranch} | wc -l`,
237
+ `git diff --name-only origin/${baseBranch}...${headRef} | wc -l`,
198
238
  { encoding: 'utf-8' }
199
239
  ).trim();
200
240
 
@@ -203,4 +243,47 @@ export class GitOperations {
203
243
  return { stats: '無法獲取統計', filesChanged: 0 };
204
244
  }
205
245
  }
246
+
247
+ /**
248
+ * 獲取當前用戶資訊
249
+ */
250
+ static getCurrentUser() {
251
+ try {
252
+ const email = execSync('git config user.email', { encoding: 'utf-8' }).trim();
253
+ const name = execSync('git config user.name', { encoding: 'utf-8' }).trim();
254
+ let githubUser = null;
255
+
256
+ try {
257
+ githubUser = execSync('gh api user --jq .login', {
258
+ encoding: 'utf-8',
259
+ stdio: ['pipe', 'pipe', 'pipe'],
260
+ }).trim();
261
+ } catch {
262
+ // GitHub CLI 未認證或未安裝
263
+ }
264
+
265
+ return { email, name, githubUser };
266
+ } catch (error) {
267
+ return null;
268
+ }
269
+ }
270
+
271
+ /**
272
+ * 獲取 repository owner
273
+ */
274
+ static getRepoOwner() {
275
+ try {
276
+ const remoteUrl = execSync('git config --get remote.origin.url', {
277
+ encoding: 'utf-8',
278
+ }).trim();
279
+
280
+ const httpsMatch = remoteUrl.match(/github\.com[/:]([^/]+)\//);
281
+ const sshMatch = remoteUrl.match(/github\.com:([^/]+)\//);
282
+
283
+ const owner = httpsMatch?.[1] || sshMatch?.[1];
284
+ return owner || null;
285
+ } catch (error) {
286
+ return null;
287
+ }
288
+ }
206
289
  }