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,16 +1,13 @@
1
1
  /**
2
- * PR 命令 - 完整複製自 scripts/ai-auto-pr.mjs
3
- *
4
- * 使用 pr-modules 的完整邏輯(從 scripts/ai-pr-modules 複製)
5
- * 確保功能與 scripts 版本完全相同
2
+ * PR 命令
3
+ * AI 自動生成 PR 並創建 Pull Request
6
4
  */
7
5
 
8
6
  import { execSync } from 'child_process';
9
7
  import { PRWorkflow } from '../pr-modules/core/workflow.js';
10
- import { loadConfig } from '../pr-modules/core/config-loader.js';
11
- import { handleError } from '../pr-modules/utils/helpers.js';
12
- import { Logger } from '../pr-modules/ui/logger.js';
13
- import { colors } from '../pr-modules/utils/constants.js';
8
+ import { loadConfig } from '../core/config-loader.js';
9
+ import { Logger } from '../utils/logger.js';
10
+ import { colors } from '../utils/constants.js';
14
11
 
15
12
  /**
16
13
  * 檢查 gh CLI 是否已登入且 token 有效,未登入則印出提示並回傳 false
@@ -34,37 +31,31 @@ function checkGHAuth(logger) {
34
31
  }
35
32
 
36
33
  /**
37
- * PR 命令主函數(完全照抄 scripts/ai-auto-pr.mjs)
34
+ * PR 命令主函數
38
35
  */
39
36
  export async function prCommand(options = {}) {
40
37
  const logger = new Logger();
41
38
 
42
- // ── 第一步:確認 gh CLI 已登入 ──────────────────────────
39
+ // 確認 gh CLI 已登入
43
40
  if (!checkGHAuth(logger)) return;
44
41
 
45
- try {
46
- logger.header('AI Auto PR Generator (v2.0 Enhanced)');
47
-
48
- // 載入配置(使用 scripts/ 的配置載入邏輯)
49
-
50
- const config = await loadConfig();
42
+ logger.header('AI Auto PR Generator (v2.0 Enhanced)');
51
43
 
52
- if (config.output.verbose) {
53
- console.log('📋 使用配置:');
54
- console.log(` AI Model: ${config.ai.model}`);
55
- console.log(` Max Diff Length: ${config.ai.maxDiffLength}`);
56
- }
44
+ // 載入配置
45
+ const config = await loadConfig();
57
46
 
58
- // 將命令行選項合併到配置中
59
- if (options.forceNew) {
60
- config.forceNew = true;
61
- }
47
+ if (config.output.verbose) {
48
+ console.log('📋 使用配置:');
49
+ console.log(` AI Model: ${config.ai.model}`);
50
+ console.log(` Max Diff Length: ${config.ai.maxDiffLength}`);
51
+ }
62
52
 
63
- // 執行工作流程(使用 scripts/ 的完整工作流)
64
- const workflow = new PRWorkflow(config);
65
- await workflow.execute();
66
- } catch (error) {
67
- handleError(error);
68
- throw error;
53
+ // 將命令行選項合併到配置中
54
+ if (options.forceNew) {
55
+ config.forceNew = true;
69
56
  }
57
+
58
+ // 執行工作流程
59
+ const workflow = new PRWorkflow(config);
60
+ await workflow.execute();
70
61
  }
@@ -0,0 +1,557 @@
1
+ /**
2
+ * Usage 命令 - 查看組織 AI Copilot 使用狀態與用量
3
+ *
4
+ * 用法:
5
+ * npx ai-git-tools usage
6
+ * npx ai-git-tools usage --from 2026-06-01 --to 2026-06-30
7
+ * npx ai-git-tools usage --top 10 --sort credits
8
+ * npx ai-git-tools usage --export usage.csv
9
+ * npx ai-git-tools usage --breakdown
10
+ * npx ai-git-tools usage --org my-org
11
+ */
12
+
13
+ import { execSync } from 'child_process';
14
+ import { writeFileSync } from 'fs';
15
+ import { Logger } from '../utils/logger.js';
16
+ import { GitHubAPI } from '../pr-modules/core/github-api.js';
17
+
18
+ // 每個 AI Credit 的費用
19
+ const CREDIT_COST = 0.01;
20
+
21
+ // ANSI 色碼
22
+ const c = {
23
+ reset: '\x1b[0m',
24
+ bright: '\x1b[1m',
25
+ dim: '\x1b[2m',
26
+ green: '\x1b[32m',
27
+ yellow: '\x1b[33m',
28
+ blue: '\x1b[34m',
29
+ red: '\x1b[31m',
30
+ cyan: '\x1b[36m',
31
+ magenta: '\x1b[35m',
32
+ white: '\x1b[37m',
33
+ };
34
+
35
+ const logger = new Logger();
36
+
37
+ // ─── 工具函式 ──────────────────────────────────────────────────────────────────
38
+
39
+ /**
40
+ * 轉換日期為 YYYY-MM-DD 字串
41
+ */
42
+ function toISODate(date) {
43
+ return date.toISOString().split('T')[0];
44
+ }
45
+
46
+ /**
47
+ * 格式化日期顯示(如 Jun 1, 2026)
48
+ */
49
+ function formatDate(date) {
50
+ return date.toLocaleDateString('en-US', {
51
+ month: 'short',
52
+ day: 'numeric',
53
+ year: 'numeric',
54
+ });
55
+ }
56
+
57
+ /**
58
+ * 格式化數字,加千位分隔符
59
+ */
60
+ function fmtNum(n, decimals = 2) {
61
+ return n.toLocaleString('en-US', {
62
+ minimumFractionDigits: decimals,
63
+ maximumFractionDigits: decimals,
64
+ });
65
+ }
66
+
67
+ // ─── GitHub API 呼叫 ────────────────────────────────────────────────────────────
68
+
69
+ /**
70
+ * 抓取組織 Copilot Billing Seats(所有席位擁有者)
71
+ */
72
+ function fetchSeats(orgName) {
73
+ try {
74
+ const raw = execSync(
75
+ `gh api "orgs/${orgName}/copilot/billing/seats" --paginate --jq '.seats // []'`,
76
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
77
+ ).trim();
78
+
79
+ // paginate 模式下可能回傳多個 JSON 陣列,逐行合併
80
+ const allSeats = [];
81
+ for (const line of raw.split('\n').filter(Boolean)) {
82
+ try {
83
+ const parsed = JSON.parse(line);
84
+ if (Array.isArray(parsed)) allSeats.push(...parsed);
85
+ } catch {
86
+ // 跳過格式異常的行
87
+ }
88
+ }
89
+ return allSeats;
90
+ } catch {
91
+ return [];
92
+ }
93
+ }
94
+
95
+ /**
96
+ * 抓取 Copilot 每日使用指標(聚合)
97
+ */
98
+ function fetchMetrics(orgName, since, until) {
99
+ try {
100
+ const raw = execSync(
101
+ `gh api "orgs/${orgName}/copilot/metrics?since=${since}&until=${until}"`,
102
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
103
+ );
104
+ const parsed = JSON.parse(raw);
105
+ return Array.isArray(parsed) ? parsed : [];
106
+ } catch {
107
+ return [];
108
+ }
109
+ }
110
+
111
+ /**
112
+ * 抓取特定團隊的成員(過濾用途)
113
+ */
114
+ function fetchTeamMembers(orgName, teamSlug) {
115
+ try {
116
+ const raw = execSync(
117
+ `gh api "orgs/${orgName}/teams/${teamSlug}/members" --paginate --jq '.[].login'`,
118
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
119
+ );
120
+ return raw.trim().split('\n').filter(Boolean);
121
+ } catch {
122
+ return null; // null 表示找不到團隊
123
+ }
124
+ }
125
+
126
+ // ─── 資料處理 ────────────────────────────────────────────────────────────────────
127
+
128
+ /**
129
+ * 從每日 metrics 彙整總量
130
+ */
131
+ function aggregateMetrics(metrics) {
132
+ let totalSuggestions = 0;
133
+ let totalAcceptances = 0;
134
+ let totalLines = 0;
135
+ let totalAcceptedLines = 0;
136
+ let totalIDEChats = 0;
137
+ let totalDotcomChats = 0;
138
+ let totalPRSummaries = 0;
139
+ let peakActiveUsers = 0;
140
+
141
+ for (const day of metrics) {
142
+ peakActiveUsers = Math.max(peakActiveUsers, day.total_active_users || 0);
143
+
144
+ // IDE 程式碼補全
145
+ for (const model of day.copilot_ide_code_completions?.models ?? []) {
146
+ for (const lang of model.languages ?? []) {
147
+ totalSuggestions += lang.total_code_suggestions || 0;
148
+ totalAcceptances += lang.total_code_acceptances || 0;
149
+ totalLines += lang.total_code_lines_suggested || 0;
150
+ totalAcceptedLines += lang.total_code_lines_accepted || 0;
151
+ }
152
+ }
153
+
154
+ // IDE Chat
155
+ for (const editor of day.copilot_ide_chat?.editors ?? []) {
156
+ for (const model of editor.models ?? []) {
157
+ totalIDEChats += model.total_chats || 0;
158
+ }
159
+ }
160
+
161
+ // Dotcom Chat(github.com 上的 Copilot Chat)
162
+ for (const model of day.copilot_dotcom_chat?.models ?? []) {
163
+ totalDotcomChats += model.total_chats || 0;
164
+ }
165
+
166
+ // PR Summaries
167
+ for (const repo of day.copilot_dotcom_pull_requests?.repositories ?? []) {
168
+ for (const model of repo.models ?? []) {
169
+ totalPRSummaries += model.total_pr_summaries_created || 0;
170
+ }
171
+ }
172
+ }
173
+
174
+ return {
175
+ totalSuggestions,
176
+ totalAcceptances,
177
+ totalLines,
178
+ totalAcceptedLines,
179
+ totalIDEChats,
180
+ totalDotcomChats,
181
+ totalPRSummaries,
182
+ peakActiveUsers,
183
+ totalChats: totalIDEChats + totalDotcomChats,
184
+ };
185
+ }
186
+
187
+ /**
188
+ * 計算每位用戶的估計用量
189
+ *
190
+ * 說明:GitHub Copilot API 目前僅提供組織級別的聚合指標,
191
+ * 不直接提供每人詳細費用。此處以「活躍用戶數」為基礎,
192
+ * 將聚合用量平均分配,作為估計依據。
193
+ *
194
+ * Credits 估算規則(近似 GitHub 定價):
195
+ * 代碼建議(每次採納) = 0.5 credit
196
+ * IDE Chat(每則) = 1.0 credit
197
+ * Dotcom Chat(每則) = 1.0 credit
198
+ * PR Summary(每份) = 5.0 credits
199
+ */
200
+ function calculateUserUsage(seats, fromDate, toDate, agg) {
201
+ const activeSeats = seats.filter(s => {
202
+ if (!s.last_activity_at) return false;
203
+ const t = new Date(s.last_activity_at);
204
+ return t >= fromDate && t <= toDate;
205
+ });
206
+
207
+ const activeCount = activeSeats.length || 1;
208
+
209
+ // 每位活躍用戶的估計用量
210
+ const creditsPerUser =
211
+ (agg.totalAcceptances * 0.5) / activeCount +
212
+ (agg.totalIDEChats * 1.0) / activeCount +
213
+ (agg.totalDotcomChats * 1.0) / activeCount +
214
+ (agg.totalPRSummaries * 5.0) / activeCount;
215
+
216
+ return seats.map(seat => {
217
+ const lastAt = seat.last_activity_at ? new Date(seat.last_activity_at) : null;
218
+ const isActive = !!lastAt && lastAt >= fromDate && lastAt <= toDate;
219
+ const credits = isActive ? Math.round(creditsPerUser * 100) / 100 : 0;
220
+
221
+ return {
222
+ login: seat.assignee?.login ?? 'unknown',
223
+ isActive,
224
+ lastActivityAt: lastAt,
225
+ lastActivityEditor: seat.last_activity_editor ?? '-',
226
+ pendingCancellation: !!seat.pending_cancellation_date,
227
+ includedCredits: credits,
228
+ additionalCredits: 0,
229
+ grossAmount: Math.round(credits * CREDIT_COST * 100) / 100,
230
+ additionalUsage: 0,
231
+ };
232
+ });
233
+ }
234
+
235
+ // ─── 表格顯示 ────────────────────────────────────────────────────────────────────
236
+
237
+ /**
238
+ * 繪製 Unicode 表格分隔線
239
+ */
240
+ function separator(widths, pos = 'middle') {
241
+ const s = {
242
+ top: ['┌', '┬', '┐', '─'],
243
+ middle: ['├', '┼', '┤', '─'],
244
+ bottom: ['└', '┴', '┘', '─'],
245
+ }[pos];
246
+ return s[0] + widths.map(w => s[3].repeat(w + 2)).join(s[1]) + s[2];
247
+ }
248
+
249
+ /**
250
+ * 繪製一列資料
251
+ */
252
+ function row(cells, widths, aligns) {
253
+ const parts = cells.map((cell, i) => {
254
+ const s = String(cell ?? '');
255
+ const w = widths[i];
256
+ return aligns[i] === 'right' ? ` ${s.padStart(w)} ` : ` ${s.padEnd(w)} `;
257
+ });
258
+ return `│${parts.join('│')}│`;
259
+ }
260
+
261
+ /**
262
+ * 顯示用量總覽表格
263
+ */
264
+ function displayUsageTable(users, options) {
265
+ const showInactive = options.inactive ?? false;
266
+ const top = options.top ? parseInt(options.top, 10) : undefined;
267
+ const sortBy = options.sort ?? 'credits';
268
+
269
+ let list = showInactive ? [...users] : users.filter(u => u.isActive);
270
+
271
+ // 排序
272
+ if (sortBy === 'credits' || sortBy === 'amount') {
273
+ list.sort((a, b) => b.includedCredits - a.includedCredits);
274
+ } else if (sortBy === 'name') {
275
+ list.sort((a, b) => a.login.localeCompare(b.login));
276
+ } else if (sortBy === 'activity') {
277
+ list.sort((a, b) => (b.lastActivityAt?.getTime() ?? 0) - (a.lastActivityAt?.getTime() ?? 0));
278
+ }
279
+
280
+ if (top) list = list.slice(0, top);
281
+
282
+ if (list.length === 0) {
283
+ logger.warning('此期間無活躍用戶資料');
284
+ return;
285
+ }
286
+
287
+ const headers = ['User', 'Included credits', 'Additional credits', 'Gross amount', 'Additional usage'];
288
+ const aligns = ['left', 'right', 'right', 'right', 'right'];
289
+
290
+ const rows = list.map(u => [
291
+ u.pendingCancellation ? `${u.login} ⚠` : u.login,
292
+ u.isActive ? fmtNum(u.includedCredits) : c.dim + '—' + c.reset,
293
+ fmtNum(u.additionalCredits),
294
+ `$${fmtNum(u.grossAmount)}`,
295
+ `$${fmtNum(u.additionalUsage)}`,
296
+ ]);
297
+
298
+ // 計算欄寬(忽略 ANSI 逸出碼)
299
+ const ANSI_PATTERN = '[\u001b\u009b][[\\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]';
300
+ const stripAnsi = s => s.replace(new RegExp(ANSI_PATTERN, 'g'), '');
301
+ const colWidths = headers.map((h) =>
302
+ Math.max(h.length, ...rows.map(r => stripAnsi(String(r[headers.indexOf(h)])).length))
303
+ );
304
+
305
+ console.log(separator(colWidths, 'top'));
306
+ console.log(row(headers.map((h) => c.bright + h + c.reset), colWidths, aligns));
307
+ console.log(separator(colWidths, 'middle'));
308
+ for (const r_ of rows) {
309
+ console.log(row(r_, colWidths, aligns));
310
+ }
311
+ console.log(separator(colWidths, 'bottom'));
312
+ }
313
+
314
+ /**
315
+ * 顯示每日使用量明細(--breakdown 選項)
316
+ */
317
+ function displayDailyBreakdown(metrics) {
318
+ if (!metrics.length) return;
319
+
320
+ console.log(`\n${c.bright}📅 每日使用量明細${c.reset}\n`);
321
+
322
+ const headers = ['Date', 'Active', 'Engaged', 'Suggestions', 'Acceptances', 'Accept %', 'IDE Chats', 'PR Summaries'];
323
+ const aligns = ['left', 'right', 'right', 'right', 'right', 'right', 'right', 'right'];
324
+
325
+ const rows = metrics.map(day => {
326
+ let sugg = 0, acc = 0, chats = 0, prs = 0;
327
+
328
+ for (const m of day.copilot_ide_code_completions?.models ?? []) {
329
+ for (const l of m.languages ?? []) {
330
+ sugg += l.total_code_suggestions || 0;
331
+ acc += l.total_code_acceptances || 0;
332
+ }
333
+ }
334
+ for (const e of day.copilot_ide_chat?.editors ?? []) {
335
+ for (const m of e.models ?? []) chats += m.total_chats || 0;
336
+ }
337
+ for (const m of day.copilot_dotcom_chat?.models ?? []) chats += m.total_chats || 0;
338
+ for (const repo of day.copilot_dotcom_pull_requests?.repositories ?? []) {
339
+ for (const m of repo.models ?? []) prs += m.total_pr_summaries_created || 0;
340
+ }
341
+
342
+ const rate = sugg > 0 ? ((acc / sugg) * 100).toFixed(1) + '%' : '—';
343
+
344
+ return [
345
+ day.date,
346
+ day.total_active_users ?? 0,
347
+ day.total_engaged_users ?? 0,
348
+ sugg.toLocaleString(),
349
+ acc.toLocaleString(),
350
+ rate,
351
+ chats.toLocaleString(),
352
+ prs.toLocaleString(),
353
+ ];
354
+ });
355
+
356
+ const colWidths = headers.map((h, i) =>
357
+ Math.max(h.length, ...rows.map(r => String(r[i]).length))
358
+ );
359
+
360
+ console.log(separator(colWidths, 'top'));
361
+ console.log(row(headers.map(h => c.bright + h + c.reset), colWidths, aligns));
362
+ console.log(separator(colWidths, 'middle'));
363
+ for (const r_ of rows) console.log(row(r_, colWidths, aligns));
364
+ console.log(separator(colWidths, 'bottom'));
365
+ }
366
+
367
+ /**
368
+ * 顯示統計摘要
369
+ */
370
+ function displaySummary(users, agg, fromDate, toDate) {
371
+ const activeUsers = users.filter(u => u.isActive);
372
+ const totalCredits = users.reduce((s, u) => s + u.includedCredits, 0);
373
+ const acceptRate = agg.totalSuggestions > 0
374
+ ? ((agg.totalAcceptances / agg.totalSuggestions) * 100).toFixed(1)
375
+ : '0.0';
376
+
377
+ console.log(`\n${c.bright}══════════════════════════════════════════════════════${c.reset}`);
378
+ console.log(`${c.bright}📊 摘要統計${c.reset} (${toISODate(fromDate)} ~ ${toISODate(toDate)})\n`);
379
+
380
+ console.log(` ${c.cyan}席位總數${c.reset} ${users.length} 人`);
381
+ console.log(` ${c.green}活躍用戶${c.reset} ${activeUsers.length} 人`);
382
+ console.log(` ${c.yellow}估計總用量${c.reset} ${fmtNum(totalCredits)} credits`);
383
+ console.log(` ${c.yellow}估計總費用${c.reset} $${fmtNum(totalCredits * CREDIT_COST)}`);
384
+
385
+ if (agg.totalSuggestions > 0 || agg.totalChats > 0) {
386
+ console.log('');
387
+ console.log(` ${c.bright}💡 Copilot 使用指標(組織合計)${c.reset}`);
388
+ console.log(` ├── 代碼建議數 ${agg.totalSuggestions.toLocaleString()}`);
389
+ console.log(` ├── 採納建議數 ${agg.totalAcceptances.toLocaleString()} (採納率 ${acceptRate}%)`);
390
+ console.log(` ├── 建議行數 ${agg.totalLines.toLocaleString()}`);
391
+ console.log(` ├── 採納行數 ${agg.totalAcceptedLines.toLocaleString()}`);
392
+ console.log(` ├── IDE Chat 次數 ${agg.totalIDEChats.toLocaleString()}`);
393
+ console.log(` ├── Web Chat 次數 ${agg.totalDotcomChats.toLocaleString()}`);
394
+ console.log(` └── PR 摘要份數 ${agg.totalPRSummaries.toLocaleString()}`);
395
+ }
396
+
397
+ console.log(`\n ${c.dim}⚠ 每用戶 Credits 為基於組織聚合指標的等比估算${c.reset}`);
398
+ console.log(` ${c.dim} 精確帳單數據請至 GitHub 組織後台 Settings → Billing 查看${c.reset}\n`);
399
+ }
400
+
401
+ /**
402
+ * 匯出為 CSV
403
+ */
404
+ function exportToCSV(users, filename) {
405
+ const headers = ['User', 'Active', 'Included credits', 'Additional credits', 'Gross amount', 'Additional usage', 'Last activity', 'Last editor', 'Pending cancellation'];
406
+ const rows = users.map(u => [
407
+ u.login,
408
+ u.isActive ? 'Yes' : 'No',
409
+ u.includedCredits.toFixed(2),
410
+ u.additionalCredits.toFixed(2),
411
+ u.grossAmount.toFixed(2),
412
+ u.additionalUsage.toFixed(2),
413
+ u.lastActivityAt ? toISODate(u.lastActivityAt) : '',
414
+ u.lastActivityEditor,
415
+ u.pendingCancellation ? 'Yes' : 'No',
416
+ ]);
417
+
418
+ const csvContent = [
419
+ headers.join(','),
420
+ ...rows.map(r => r.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(',')),
421
+ ].join('\n');
422
+
423
+ writeFileSync(filename, csvContent, 'utf-8');
424
+ logger.success(`已匯出至 ${filename}(共 ${users.length} 筆)`);
425
+ }
426
+
427
+ // ─── 主函式 ────────────────────────────────────────────────────────────────────
428
+
429
+ /**
430
+ * Usage 命令主函式
431
+ */
432
+ export async function usageCommand(options = {}) {
433
+ const githubAPI = new GitHubAPI();
434
+ const orgName = options.org || githubAPI.orgName;
435
+
436
+ if (!orgName) {
437
+ logger.error('無法偵測組織名稱');
438
+ console.log(`請使用 ${c.cyan}--org <org-name>${c.reset} 指定組織,或在 Git repo 根目錄下執行`);
439
+ process.exit(1);
440
+ }
441
+
442
+ // 驗證 gh CLI 登入
443
+ const authStatus = githubAPI.checkAuth();
444
+ if (!authStatus.authenticated) {
445
+ logger.error('GitHub CLI 未登入或 token 已失效');
446
+ console.log(`\n請先執行:${c.green}gh auth login${c.reset}`);
447
+ console.log('登入時請確保選取 scope:');
448
+ console.log(' · manage_billing:copilot(查看用量資訊)');
449
+ console.log(' · read:org(查看成員列表)\n');
450
+ process.exit(1);
451
+ }
452
+
453
+ // 計算日期範圍(預設:本月)
454
+ const now = new Date();
455
+ const fromDate = options.from ? new Date(options.from) : new Date(now.getFullYear(), now.getMonth(), 1);
456
+ const toDate = options.to ? new Date(options.to) : now;
457
+ const fromStr = toISODate(fromDate);
458
+ const toStr = toISODate(toDate);
459
+
460
+ // ── 標題 ──────────────────────────────────────────────────
461
+ console.log('');
462
+ console.log(`${c.bright}Usage breakdown${c.reset}`);
463
+ console.log(`${c.dim}Usage for ${formatDate(fromDate)} - ${formatDate(toDate)}. Each AI credit costs $${CREDIT_COST.toFixed(2)}.${c.reset}`);
464
+ console.log('');
465
+
466
+ try {
467
+ // 1. 抓取 Seats
468
+ logger.step(`抓取 ${c.cyan}${orgName}${c.reset} 的 Copilot Seats...`);
469
+ let seats = fetchSeats(orgName);
470
+
471
+ if (seats.length === 0) {
472
+ logger.warning('無法取得 Seats 資料(可能需要 manage_billing:copilot 權限)');
473
+ logger.warning('嘗試改用組織成員列表...');
474
+ const members = await githubAPI.fetchOrgMembers(orgName);
475
+ // 將 members 轉換為 seats 結構(無 last_activity_at)
476
+ seats = members.map(m => ({
477
+ assignee: { login: m.login },
478
+ last_activity_at: null,
479
+ last_activity_editor: null,
480
+ pending_cancellation_date: null,
481
+ }));
482
+ }
483
+
484
+ // 2. 套用 Team 過濾(若指定 --team)
485
+ if (options.team) {
486
+ logger.step(`套用團隊過濾:${options.team}...`);
487
+ const teamMembers = fetchTeamMembers(orgName, options.team);
488
+ if (teamMembers === null) {
489
+ logger.error(`找不到團隊 "${options.team}"`);
490
+ process.exit(1);
491
+ }
492
+ const teamSet = new Set(teamMembers);
493
+ seats = seats.filter(s => teamSet.has(s.assignee?.login));
494
+ if (seats.length === 0) {
495
+ logger.warning(`團隊 "${options.team}" 中無 Copilot Seat`);
496
+ return;
497
+ }
498
+ }
499
+
500
+ // 3. 抓取使用指標
501
+ logger.step('抓取 Copilot 使用指標...');
502
+ const metrics = fetchMetrics(orgName, fromStr, toStr);
503
+ if (metrics.length === 0) {
504
+ logger.warning('無法取得 Copilot Metrics 資料(可能需要 GitHub Copilot Enterprise 方案)');
505
+ }
506
+
507
+ // 4. 計算用量
508
+ const agg = aggregateMetrics(metrics);
509
+ const usageData = calculateUserUsage(seats, fromDate, toDate, agg);
510
+
511
+ // 5. 顯示表格
512
+ console.log('');
513
+ displayUsageTable(usageData, options);
514
+
515
+ // 6. 顯示摘要
516
+ displaySummary(usageData, agg, fromDate, toDate);
517
+
518
+ // 7. 每日明細(可選)
519
+ if (options.breakdown) {
520
+ displayDailyBreakdown(metrics);
521
+ }
522
+
523
+ // 8. JSON 輸出(可選)
524
+ if (options.json) {
525
+ const jsonOutput = {
526
+ org: orgName,
527
+ period: { from: fromStr, to: toStr },
528
+ summary: {
529
+ totalSeats: usageData.length,
530
+ activeUsers: usageData.filter(u => u.isActive).length,
531
+ totalCredits: usageData.reduce((s, u) => s + u.includedCredits, 0),
532
+ ...agg,
533
+ },
534
+ users: usageData,
535
+ dailyMetrics: metrics,
536
+ };
537
+ console.log(JSON.stringify(jsonOutput, null, 2));
538
+ }
539
+
540
+ // 9. 匯出 CSV(可選)
541
+ if (options.export) {
542
+ exportToCSV(usageData, options.export);
543
+ }
544
+
545
+ } catch (error) {
546
+ logger.error(`執行失敗:${error.message}`);
547
+ console.log('\n排除問題:');
548
+ console.log(' 1. 確認已執行:gh auth login');
549
+ console.log(' 2. 確認 Token 包含 manage_billing:copilot 或 read:org scope');
550
+ console.log(` 3. 確認有權限存取組織 ${orgName}`);
551
+ console.log(' 4. 確認組織已啟用 GitHub Copilot');
552
+ if (process.env.DEBUG) {
553
+ console.error(error);
554
+ }
555
+ process.exit(1);
556
+ }
557
+ }