@mcp-consultant-tools/github-enterprise 1.0.0

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,1101 @@
1
+ import { Octokit } from '@octokit/rest';
2
+ import { createAppAuth } from '@octokit/auth-app';
3
+ import axios from 'axios';
4
+ import { auditLogger } from '@mcp-consultant-tools/core';
5
+ /**
6
+ * GitHub Enterprise Service
7
+ * Manages authentication, API requests, caching, and branch selection for GitHub Enterprise Cloud
8
+ */
9
+ export class GitHubEnterpriseService {
10
+ config;
11
+ baseApiUrl;
12
+ octokit = null;
13
+ // Token caching (for GitHub App)
14
+ accessToken = null;
15
+ tokenExpirationTime = 0;
16
+ // Response caching
17
+ cache = new Map();
18
+ constructor(config) {
19
+ this.config = config;
20
+ this.baseApiUrl = `${config.baseUrl}/api/v3`;
21
+ // Initialize Octokit based on auth method
22
+ this.initializeOctokit();
23
+ }
24
+ /**
25
+ * Initialize Octokit client based on authentication method
26
+ */
27
+ initializeOctokit() {
28
+ try {
29
+ if (this.config.authMethod === 'pat') {
30
+ // PAT authentication (primary method)
31
+ this.octokit = new Octokit({
32
+ auth: this.config.pat,
33
+ baseUrl: this.baseApiUrl,
34
+ userAgent: 'mcp-consultant-tools',
35
+ });
36
+ }
37
+ else if (this.config.authMethod === 'github-app') {
38
+ // GitHub App authentication (optional/advanced)
39
+ if (!this.config.appId || !this.config.appPrivateKey || !this.config.appInstallationId) {
40
+ throw new Error('GitHub App authentication requires appId, appPrivateKey, and appInstallationId');
41
+ }
42
+ this.octokit = new Octokit({
43
+ authStrategy: createAppAuth,
44
+ auth: {
45
+ appId: this.config.appId,
46
+ privateKey: this.config.appPrivateKey,
47
+ installationId: this.config.appInstallationId,
48
+ },
49
+ baseUrl: this.baseApiUrl,
50
+ userAgent: 'mcp-consultant-tools',
51
+ });
52
+ }
53
+ else {
54
+ throw new Error(`Unsupported authentication method: ${this.config.authMethod}`);
55
+ }
56
+ }
57
+ catch (error) {
58
+ console.error('Failed to initialize Octokit:', error.message);
59
+ throw error;
60
+ }
61
+ }
62
+ /**
63
+ * Get access token with caching (for GitHub App auth)
64
+ * Implements 5-minute buffer pattern before expiry
65
+ */
66
+ async getAccessToken() {
67
+ if (this.config.authMethod === 'pat') {
68
+ return this.config.pat;
69
+ }
70
+ const currentTime = Date.now();
71
+ // Return cached token if still valid (with 5 minute buffer)
72
+ if (this.accessToken && this.tokenExpirationTime > currentTime) {
73
+ return this.accessToken;
74
+ }
75
+ // Acquire new token for GitHub App
76
+ try {
77
+ const auth = await this.octokit.auth({ type: 'installation' });
78
+ if (!auth.token) {
79
+ throw new Error('GitHub App auth did not return a token');
80
+ }
81
+ const token = auth.token;
82
+ this.accessToken = token;
83
+ // GitHub App installation tokens expire after 1 hour
84
+ // Set expiration time (subtract 5 minutes to refresh early)
85
+ this.tokenExpirationTime = currentTime + (55 * 60 * 1000); // 55 minutes
86
+ return token;
87
+ }
88
+ catch (error) {
89
+ console.error('Failed to acquire GitHub App installation token:', error.message);
90
+ throw new Error(`Failed to acquire GitHub App token: ${error.message}`);
91
+ }
92
+ }
93
+ /**
94
+ * Get cache key for a request
95
+ */
96
+ getCacheKey(method, repo, resource, params) {
97
+ const paramStr = params ? JSON.stringify(params) : '';
98
+ return `${method}:${repo}:${resource}:${paramStr}`;
99
+ }
100
+ /**
101
+ * Get cached response
102
+ */
103
+ getCached(key) {
104
+ const cached = this.cache.get(key);
105
+ if (cached && Date.now() < cached.expires) {
106
+ return cached.data;
107
+ }
108
+ this.cache.delete(key); // Expired - remove it
109
+ return null;
110
+ }
111
+ /**
112
+ * Set cache entry
113
+ */
114
+ setCache(key, data, ttlSeconds) {
115
+ if (!this.config.enableCache)
116
+ return;
117
+ const ttl = ttlSeconds || this.config.cacheTtl;
118
+ this.cache.set(key, {
119
+ data,
120
+ expires: Date.now() + (ttl * 1000)
121
+ });
122
+ }
123
+ /**
124
+ * Clear cache entries
125
+ * @param pattern Optional pattern to match cache keys
126
+ * @param repoId Optional repo ID to clear cache for specific repo
127
+ * @returns Number of cache entries cleared
128
+ */
129
+ clearCache(pattern, repoId) {
130
+ if (repoId) {
131
+ const repo = this.getRepoById(repoId);
132
+ const repoPattern = `${repo.owner}/${repo.repo}`;
133
+ pattern = pattern ? `${repoPattern}:${pattern}` : repoPattern;
134
+ }
135
+ if (pattern) {
136
+ let cleared = 0;
137
+ for (const key of this.cache.keys()) {
138
+ if (key.includes(pattern)) {
139
+ this.cache.delete(key);
140
+ cleared++;
141
+ }
142
+ }
143
+ console.error(`Cleared ${cleared} cache entries matching pattern '${pattern}'`);
144
+ return cleared;
145
+ }
146
+ const size = this.cache.size;
147
+ this.cache.clear();
148
+ console.error(`Cleared all ${size} cache entries`);
149
+ return size;
150
+ }
151
+ /**
152
+ * Make API request with error handling and caching
153
+ */
154
+ async makeRequest(endpoint, options = {}) {
155
+ const { method = 'GET', data, useCache = true, cacheTtl, repoId } = options;
156
+ // Check cache for GET requests
157
+ if (method === 'GET' && useCache && this.config.enableCache) {
158
+ const cacheKey = this.getCacheKey(method, repoId || '', endpoint, data);
159
+ const cached = this.getCached(cacheKey);
160
+ if (cached) {
161
+ return cached;
162
+ }
163
+ }
164
+ try {
165
+ const token = await this.getAccessToken();
166
+ const url = endpoint.startsWith('http') ? endpoint : `${this.baseApiUrl}/${endpoint}`;
167
+ const response = await axios({
168
+ method,
169
+ url,
170
+ headers: {
171
+ 'Authorization': `token ${token}`,
172
+ 'Accept': 'application/vnd.github.v3+json',
173
+ 'X-GitHub-Api-Version': this.config.apiVersion,
174
+ 'Content-Type': 'application/json',
175
+ },
176
+ data,
177
+ });
178
+ // Cache successful GET responses
179
+ if (method === 'GET' && useCache && this.config.enableCache) {
180
+ const cacheKey = this.getCacheKey(method, repoId || '', endpoint, data);
181
+ this.setCache(cacheKey, response.data, cacheTtl);
182
+ }
183
+ return response.data;
184
+ }
185
+ catch (error) {
186
+ // Comprehensive error handling
187
+ let errorMessage = 'Unknown error';
188
+ let errorDetails = {};
189
+ if (error.response) {
190
+ const status = error.response.status;
191
+ const data = error.response.data;
192
+ switch (status) {
193
+ case 401:
194
+ errorMessage = 'Authentication failed. Check your PAT or GitHub App credentials.';
195
+ break;
196
+ case 403:
197
+ if (error.response.headers['x-ratelimit-remaining'] === '0') {
198
+ const resetTime = error.response.headers['x-ratelimit-reset'];
199
+ const resetDate = resetTime ? new Date(parseInt(resetTime) * 1000).toLocaleString() : 'unknown';
200
+ errorMessage = `Rate limit exceeded. Resets at ${resetDate}.`;
201
+ }
202
+ else {
203
+ errorMessage = 'Access denied. Check repository permissions.';
204
+ }
205
+ break;
206
+ case 404:
207
+ errorMessage = `Resource not found: ${endpoint}`;
208
+ break;
209
+ case 422:
210
+ errorMessage = `Validation failed: ${data?.message || 'Invalid request parameters'}`;
211
+ break;
212
+ default:
213
+ errorMessage = `HTTP ${status}: ${data?.message || error.message}`;
214
+ }
215
+ errorDetails = { status, message: data?.message };
216
+ }
217
+ else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
218
+ errorMessage = `Network error: Unable to reach GitHub Enterprise at ${this.config.baseUrl}. Check your connection and GHE_URL.`;
219
+ }
220
+ else if (error.code === 'ETIMEDOUT') {
221
+ errorMessage = 'Request timeout. GitHub Enterprise API is slow to respond.';
222
+ }
223
+ else {
224
+ errorMessage = error.message;
225
+ }
226
+ console.error('GitHub Enterprise API request failed:', { endpoint, method, status: error.response?.status, error: errorMessage });
227
+ throw new Error(errorMessage);
228
+ }
229
+ }
230
+ /**
231
+ * Get all configured repositories
232
+ */
233
+ getAllRepos() {
234
+ return this.config.repos;
235
+ }
236
+ /**
237
+ * Get active repositories only
238
+ */
239
+ getActiveRepos() {
240
+ return this.config.repos.filter(r => r.active);
241
+ }
242
+ /**
243
+ * Get repository by ID with validation
244
+ */
245
+ getRepoById(repoId) {
246
+ const repo = this.config.repos.find(r => r.id === repoId);
247
+ if (!repo) {
248
+ const availableIds = this.config.repos.map(r => r.id).join(', ');
249
+ throw new Error(`Repository '${repoId}' not found. Available repositories: ${availableIds || 'none'}`);
250
+ }
251
+ if (!repo.active) {
252
+ throw new Error(`Repository '${repoId}' is inactive. Set 'active: true' in configuration to enable it.`);
253
+ }
254
+ return repo;
255
+ }
256
+ /**
257
+ * List all branches for a repository
258
+ */
259
+ async listBranches(repoId, protectedOnly) {
260
+ const timer = auditLogger.startTimer();
261
+ const repo = this.getRepoById(repoId);
262
+ try {
263
+ const branches = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/branches`, { repoId });
264
+ const filteredBranches = protectedOnly !== undefined
265
+ ? branches.filter(b => b.protected === protectedOnly)
266
+ : branches;
267
+ auditLogger.log({
268
+ operation: 'list-branches',
269
+ operationType: 'READ',
270
+ componentType: 'Branch',
271
+ success: true,
272
+ parameters: { repoId, protectedOnly },
273
+ executionTimeMs: timer(),
274
+ });
275
+ return filteredBranches;
276
+ }
277
+ catch (error) {
278
+ auditLogger.log({
279
+ operation: 'list-branches',
280
+ operationType: 'READ',
281
+ componentType: 'Branch',
282
+ success: false,
283
+ error: error.message,
284
+ parameters: { repoId },
285
+ executionTimeMs: timer(),
286
+ });
287
+ throw error;
288
+ }
289
+ }
290
+ /**
291
+ * Auto-detect default branch for a repository
292
+ * Handles typos gracefully and provides alternatives
293
+ */
294
+ async getDefaultBranch(repoId, userSpecified) {
295
+ const repo = this.getRepoById(repoId);
296
+ // 1. User explicitly specified branch (highest priority)
297
+ if (userSpecified) {
298
+ const branches = await this.listBranches(repoId);
299
+ const exists = branches.find(b => b.name === userSpecified);
300
+ if (exists) {
301
+ return {
302
+ branch: userSpecified,
303
+ reason: 'user-specified',
304
+ confidence: 'high'
305
+ };
306
+ }
307
+ // Branch doesn't exist - show available branches
308
+ const availableBranches = branches.map(b => ` - ${b.name}`).join('\n');
309
+ throw new Error(`Branch "${userSpecified}" not found in ${repo.owner}/${repo.repo}.\n\n` +
310
+ `Available branches:\n${availableBranches}`);
311
+ }
312
+ // 2. Check if default branch configured for this repo
313
+ if (repo.defaultBranch) {
314
+ return {
315
+ branch: repo.defaultBranch,
316
+ reason: 'configured default',
317
+ confidence: 'high'
318
+ };
319
+ }
320
+ // 3. Get all branches
321
+ const branches = await this.listBranches(repoId);
322
+ // 4. Filter and sort release branches (handle typos gracefully)
323
+ const releaseBranches = branches
324
+ .filter(b => b.name.toLowerCase().startsWith('release/')) // Case-insensitive
325
+ .map(b => {
326
+ // Parse version number after "release/"
327
+ const versionStr = b.name.substring(b.name.indexOf('/') + 1);
328
+ const version = parseFloat(versionStr);
329
+ return {
330
+ name: b.name,
331
+ version: isNaN(version) ? 0 : version,
332
+ raw: versionStr
333
+ };
334
+ })
335
+ .filter(b => b.version > 0) // Only keep valid version numbers
336
+ .sort((a, b) => b.version - a.version); // Highest first
337
+ // 5. Auto-select highest version, but ALWAYS show alternatives
338
+ if (releaseBranches.length > 0) {
339
+ const selected = releaseBranches[0].name;
340
+ const allAlternatives = releaseBranches.slice(1).map(b => b.name);
341
+ console.error(`✓ Auto-selected branch: ${selected} (highest release version ${releaseBranches[0].version})`);
342
+ if (allAlternatives.length > 0) {
343
+ console.error(` Alternatives: ${allAlternatives.slice(0, 3).join(', ')}${allAlternatives.length > 3 ? '...' : ''}`);
344
+ }
345
+ return {
346
+ branch: selected,
347
+ reason: `auto-detected: highest release version (${releaseBranches[0].version})`,
348
+ confidence: 'medium',
349
+ alternatives: allAlternatives,
350
+ message: `Auto-selected "${selected}". If this is incorrect, specify a different branch explicitly.`
351
+ };
352
+ }
353
+ // 6. No release branches found - fallback to main/master
354
+ console.error(`⚠️ No release branches found in format "release/X.Y" for ${repo.owner}/${repo.repo}`);
355
+ const availableBranchNames = branches.map(b => b.name);
356
+ console.error(` Available branches: ${availableBranchNames.slice(0, 5).join(', ')}${availableBranchNames.length > 5 ? '...' : ''}`);
357
+ const mainBranch = branches.find(b => b.name === 'main' || b.name === 'master');
358
+ if (mainBranch) {
359
+ console.error(`⚠️ Falling back to: ${mainBranch.name} (main branch - likely production)`);
360
+ return {
361
+ branch: mainBranch.name,
362
+ reason: 'fallback to main branch (no release branches found)',
363
+ confidence: 'low',
364
+ alternatives: availableBranchNames.filter(n => n !== mainBranch.name),
365
+ message: `No release branches found. Using "${mainBranch.name}" as fallback. User should verify this is correct.`
366
+ };
367
+ }
368
+ // 7. Cannot determine - list all branches and throw error
369
+ const branchList = availableBranchNames.map(n => ` - ${n}`).join('\n');
370
+ throw new Error(`Could not determine default branch for ${repo.owner}/${repo.repo}.\n\n` +
371
+ `Available branches:\n${branchList}\n\n` +
372
+ `Please specify a branch explicitly or configure a defaultBranch in GHE_REPOS.`);
373
+ }
374
+ /**
375
+ * Get file content from a repository
376
+ */
377
+ async getFile(repoId, path, branch) {
378
+ const timer = auditLogger.startTimer();
379
+ const repo = this.getRepoById(repoId);
380
+ try {
381
+ // Auto-detect branch if not specified
382
+ const selectedBranch = branch || (await this.getDefaultBranch(repoId)).branch;
383
+ const file = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/contents/${path}?ref=${selectedBranch}`, { repoId });
384
+ // Check file size
385
+ if (file.size > this.config.maxFileSize) {
386
+ throw new Error(`File size (${file.size} bytes) exceeds maximum allowed size (${this.config.maxFileSize} bytes). ` +
387
+ `Increase GHE_MAX_FILE_SIZE if needed.`);
388
+ }
389
+ // Decode base64 content
390
+ if (file.encoding === 'base64') {
391
+ file.decodedContent = Buffer.from(file.content, 'base64').toString('utf-8');
392
+ }
393
+ auditLogger.log({
394
+ operation: 'get-file',
395
+ operationType: 'READ',
396
+ componentType: 'File',
397
+ componentName: path,
398
+ success: true,
399
+ parameters: { repoId, path, branch: selectedBranch },
400
+ executionTimeMs: timer(),
401
+ });
402
+ return { ...file, branch: selectedBranch };
403
+ }
404
+ catch (error) {
405
+ auditLogger.log({
406
+ operation: 'get-file',
407
+ operationType: 'READ',
408
+ componentType: 'File',
409
+ componentName: path,
410
+ success: false,
411
+ error: error.message,
412
+ parameters: { repoId, path, branch },
413
+ executionTimeMs: timer(),
414
+ });
415
+ throw error;
416
+ }
417
+ }
418
+ /**
419
+ * Search code across repositories
420
+ */
421
+ async searchCode(query, repoId, path, extension) {
422
+ const timer = auditLogger.startTimer();
423
+ try {
424
+ // Build search query
425
+ let searchQuery = query;
426
+ if (repoId) {
427
+ const repo = this.getRepoById(repoId);
428
+ searchQuery += ` repo:${repo.owner}/${repo.repo}`;
429
+ }
430
+ if (path) {
431
+ searchQuery += ` path:${path}`;
432
+ }
433
+ if (extension) {
434
+ searchQuery += ` extension:${extension}`;
435
+ }
436
+ const result = await this.makeRequest(`search/code?q=${encodeURIComponent(searchQuery)}&per_page=${this.config.maxSearchResults}`, { useCache: false } // Don't cache search results
437
+ );
438
+ auditLogger.log({
439
+ operation: 'search-code',
440
+ operationType: 'READ',
441
+ componentType: 'Code',
442
+ success: true,
443
+ parameters: { query, repoId, path, extension, totalResults: result.total_count },
444
+ executionTimeMs: timer(),
445
+ });
446
+ return result;
447
+ }
448
+ catch (error) {
449
+ auditLogger.log({
450
+ operation: 'search-code',
451
+ operationType: 'READ',
452
+ componentType: 'Code',
453
+ success: false,
454
+ error: error.message,
455
+ parameters: { query, repoId, path, extension },
456
+ executionTimeMs: timer(),
457
+ });
458
+ throw error;
459
+ }
460
+ }
461
+ /**
462
+ * List files in a directory
463
+ */
464
+ async listFiles(repoId, path, branch) {
465
+ const timer = auditLogger.startTimer();
466
+ const repo = this.getRepoById(repoId);
467
+ try {
468
+ // Auto-detect branch if not specified
469
+ const selectedBranch = branch || (await this.getDefaultBranch(repoId)).branch;
470
+ const dirPath = path || '';
471
+ const contents = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/contents/${dirPath}?ref=${selectedBranch}`, { repoId });
472
+ auditLogger.log({
473
+ operation: 'list-files',
474
+ operationType: 'READ',
475
+ componentType: 'Directory',
476
+ componentName: path || '/',
477
+ success: true,
478
+ parameters: { repoId, path, branch: selectedBranch },
479
+ executionTimeMs: timer(),
480
+ });
481
+ return { contents, branch: selectedBranch };
482
+ }
483
+ catch (error) {
484
+ auditLogger.log({
485
+ operation: 'list-files',
486
+ operationType: 'READ',
487
+ componentType: 'Directory',
488
+ componentName: path || '/',
489
+ success: false,
490
+ error: error.message,
491
+ parameters: { repoId, path, branch },
492
+ executionTimeMs: timer(),
493
+ });
494
+ throw error;
495
+ }
496
+ }
497
+ /**
498
+ * Get commit history for a branch
499
+ */
500
+ async getCommits(repoId, branch, since, until, author, path, limit = 50) {
501
+ const timer = auditLogger.startTimer();
502
+ const repo = this.getRepoById(repoId);
503
+ try {
504
+ // Auto-detect branch if not specified
505
+ const selectedBranch = branch || (await this.getDefaultBranch(repoId)).branch;
506
+ // Build query parameters
507
+ const params = {
508
+ sha: selectedBranch,
509
+ per_page: limit,
510
+ };
511
+ if (since)
512
+ params.since = since;
513
+ if (until)
514
+ params.until = until;
515
+ if (author)
516
+ params.author = author;
517
+ if (path)
518
+ params.path = path;
519
+ const queryString = new URLSearchParams(params).toString();
520
+ const commits = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/commits?${queryString}`, { repoId });
521
+ auditLogger.log({
522
+ operation: 'get-commits',
523
+ operationType: 'READ',
524
+ componentType: 'Commit',
525
+ success: true,
526
+ parameters: { repoId, branch: selectedBranch, since, until, author, path, limit, count: commits.length },
527
+ executionTimeMs: timer(),
528
+ });
529
+ return commits;
530
+ }
531
+ catch (error) {
532
+ auditLogger.log({
533
+ operation: 'get-commits',
534
+ operationType: 'READ',
535
+ componentType: 'Commit',
536
+ success: false,
537
+ error: error.message,
538
+ parameters: { repoId, branch, since, until, author, path, limit },
539
+ executionTimeMs: timer(),
540
+ });
541
+ throw error;
542
+ }
543
+ }
544
+ /**
545
+ * Get commit details
546
+ */
547
+ async getCommitDetails(repoId, sha) {
548
+ const timer = auditLogger.startTimer();
549
+ const repo = this.getRepoById(repoId);
550
+ try {
551
+ const commit = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/commits/${sha}`, { repoId });
552
+ auditLogger.log({
553
+ operation: 'get-commit-details',
554
+ operationType: 'READ',
555
+ componentType: 'Commit',
556
+ componentId: sha,
557
+ success: true,
558
+ parameters: { repoId, sha },
559
+ executionTimeMs: timer(),
560
+ });
561
+ return commit;
562
+ }
563
+ catch (error) {
564
+ auditLogger.log({
565
+ operation: 'get-commit-details',
566
+ operationType: 'READ',
567
+ componentType: 'Commit',
568
+ componentId: sha,
569
+ success: false,
570
+ error: error.message,
571
+ parameters: { repoId, sha },
572
+ executionTimeMs: timer(),
573
+ });
574
+ throw error;
575
+ }
576
+ }
577
+ /**
578
+ * Search commits by message
579
+ */
580
+ async searchCommits(query, repoId, author, since, until) {
581
+ const timer = auditLogger.startTimer();
582
+ try {
583
+ // Build search query
584
+ let searchQuery = query;
585
+ if (repoId) {
586
+ const repo = this.getRepoById(repoId);
587
+ searchQuery += ` repo:${repo.owner}/${repo.repo}`;
588
+ }
589
+ if (author) {
590
+ searchQuery += ` author:${author}`;
591
+ }
592
+ if (since) {
593
+ searchQuery += ` committer-date:>=${since}`;
594
+ }
595
+ if (until) {
596
+ searchQuery += ` committer-date:<=${until}`;
597
+ }
598
+ const result = await this.makeRequest(`search/commits?q=${encodeURIComponent(searchQuery)}`, { useCache: false } // Don't cache search results
599
+ );
600
+ auditLogger.log({
601
+ operation: 'search-commits',
602
+ operationType: 'READ',
603
+ componentType: 'Commit',
604
+ success: true,
605
+ parameters: { query, repoId, author, since, until, totalResults: result.total_count },
606
+ executionTimeMs: timer(),
607
+ });
608
+ return result;
609
+ }
610
+ catch (error) {
611
+ auditLogger.log({
612
+ operation: 'search-commits',
613
+ operationType: 'READ',
614
+ componentType: 'Commit',
615
+ success: false,
616
+ error: error.message,
617
+ parameters: { query, repoId, author, since, until },
618
+ executionTimeMs: timer(),
619
+ });
620
+ throw error;
621
+ }
622
+ }
623
+ /**
624
+ * Compare two branches
625
+ */
626
+ async compareBranches(repoId, base, head) {
627
+ const timer = auditLogger.startTimer();
628
+ const repo = this.getRepoById(repoId);
629
+ try {
630
+ const comparison = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/compare/${base}...${head}`, { repoId });
631
+ auditLogger.log({
632
+ operation: 'compare-branches',
633
+ operationType: 'READ',
634
+ componentType: 'Branch',
635
+ success: true,
636
+ parameters: { repoId, base, head, aheadBy: comparison.ahead_by, behindBy: comparison.behind_by },
637
+ executionTimeMs: timer(),
638
+ });
639
+ return comparison;
640
+ }
641
+ catch (error) {
642
+ auditLogger.log({
643
+ operation: 'compare-branches',
644
+ operationType: 'READ',
645
+ componentType: 'Branch',
646
+ success: false,
647
+ error: error.message,
648
+ parameters: { repoId, base, head },
649
+ executionTimeMs: timer(),
650
+ });
651
+ throw error;
652
+ }
653
+ }
654
+ /**
655
+ * Get branch details
656
+ */
657
+ async getBranchDetails(repoId, branch) {
658
+ const timer = auditLogger.startTimer();
659
+ const repo = this.getRepoById(repoId);
660
+ try {
661
+ const branchInfo = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/branches/${branch}`, { repoId });
662
+ auditLogger.log({
663
+ operation: 'get-branch-details',
664
+ operationType: 'READ',
665
+ componentType: 'Branch',
666
+ componentName: branch,
667
+ success: true,
668
+ parameters: { repoId, branch },
669
+ executionTimeMs: timer(),
670
+ });
671
+ return branchInfo;
672
+ }
673
+ catch (error) {
674
+ auditLogger.log({
675
+ operation: 'get-branch-details',
676
+ operationType: 'READ',
677
+ componentType: 'Branch',
678
+ componentName: branch,
679
+ success: false,
680
+ error: error.message,
681
+ parameters: { repoId, branch },
682
+ executionTimeMs: timer(),
683
+ });
684
+ throw error;
685
+ }
686
+ }
687
+ /**
688
+ * List pull requests
689
+ */
690
+ async listPullRequests(repoId, state = 'open', base, head, sort = 'created', limit = 30) {
691
+ const timer = auditLogger.startTimer();
692
+ const repo = this.getRepoById(repoId);
693
+ try {
694
+ const params = {
695
+ state,
696
+ sort,
697
+ per_page: limit,
698
+ };
699
+ if (base)
700
+ params.base = base;
701
+ if (head)
702
+ params.head = head;
703
+ const queryString = new URLSearchParams(params).toString();
704
+ const prs = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/pulls?${queryString}`, { repoId });
705
+ auditLogger.log({
706
+ operation: 'list-pull-requests',
707
+ operationType: 'READ',
708
+ componentType: 'PullRequest',
709
+ success: true,
710
+ parameters: { repoId, state, base, head, sort, limit, count: prs.length },
711
+ executionTimeMs: timer(),
712
+ });
713
+ return prs;
714
+ }
715
+ catch (error) {
716
+ auditLogger.log({
717
+ operation: 'list-pull-requests',
718
+ operationType: 'READ',
719
+ componentType: 'PullRequest',
720
+ success: false,
721
+ error: error.message,
722
+ parameters: { repoId, state, base, head, sort, limit },
723
+ executionTimeMs: timer(),
724
+ });
725
+ throw error;
726
+ }
727
+ }
728
+ /**
729
+ * Get pull request details
730
+ */
731
+ async getPullRequest(repoId, prNumber) {
732
+ const timer = auditLogger.startTimer();
733
+ const repo = this.getRepoById(repoId);
734
+ try {
735
+ const pr = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/pulls/${prNumber}`, { repoId });
736
+ auditLogger.log({
737
+ operation: 'get-pull-request',
738
+ operationType: 'READ',
739
+ componentType: 'PullRequest',
740
+ componentId: prNumber.toString(),
741
+ success: true,
742
+ parameters: { repoId, prNumber },
743
+ executionTimeMs: timer(),
744
+ });
745
+ return pr;
746
+ }
747
+ catch (error) {
748
+ auditLogger.log({
749
+ operation: 'get-pull-request',
750
+ operationType: 'READ',
751
+ componentType: 'PullRequest',
752
+ componentId: prNumber.toString(),
753
+ success: false,
754
+ error: error.message,
755
+ parameters: { repoId, prNumber },
756
+ executionTimeMs: timer(),
757
+ });
758
+ throw error;
759
+ }
760
+ }
761
+ /**
762
+ * Get pull request files
763
+ */
764
+ async getPullRequestFiles(repoId, prNumber) {
765
+ const timer = auditLogger.startTimer();
766
+ const repo = this.getRepoById(repoId);
767
+ try {
768
+ const files = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/pulls/${prNumber}/files`, { repoId });
769
+ auditLogger.log({
770
+ operation: 'get-pr-files',
771
+ operationType: 'READ',
772
+ componentType: 'PullRequest',
773
+ componentId: prNumber.toString(),
774
+ success: true,
775
+ parameters: { repoId, prNumber, fileCount: files.length },
776
+ executionTimeMs: timer(),
777
+ });
778
+ return files;
779
+ }
780
+ catch (error) {
781
+ auditLogger.log({
782
+ operation: 'get-pr-files',
783
+ operationType: 'READ',
784
+ componentType: 'PullRequest',
785
+ componentId: prNumber.toString(),
786
+ success: false,
787
+ error: error.message,
788
+ parameters: { repoId, prNumber },
789
+ executionTimeMs: timer(),
790
+ });
791
+ throw error;
792
+ }
793
+ }
794
+ /**
795
+ * Create a new branch (requires GHE_ENABLE_CREATE=true)
796
+ */
797
+ async createBranch(repoId, branchName, fromBranch) {
798
+ if (!this.config.enableCreate) {
799
+ throw new Error('Branch creation is disabled. Set GHE_ENABLE_CREATE=true to enable.');
800
+ }
801
+ const timer = auditLogger.startTimer();
802
+ const repo = this.getRepoById(repoId);
803
+ try {
804
+ // Get source branch SHA
805
+ const sourceBranch = fromBranch || (await this.getDefaultBranch(repoId)).branch;
806
+ const branchInfo = await this.getBranchDetails(repoId, sourceBranch);
807
+ const sha = branchInfo.commit.sha;
808
+ // Create new branch
809
+ const result = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/git/refs`, {
810
+ method: 'POST',
811
+ data: {
812
+ ref: `refs/heads/${branchName}`,
813
+ sha,
814
+ },
815
+ useCache: false,
816
+ });
817
+ auditLogger.log({
818
+ operation: 'create-branch',
819
+ operationType: 'CREATE',
820
+ componentType: 'Branch',
821
+ componentName: branchName,
822
+ success: true,
823
+ parameters: { repoId, branchName, fromBranch: sourceBranch },
824
+ executionTimeMs: timer(),
825
+ });
826
+ return result;
827
+ }
828
+ catch (error) {
829
+ auditLogger.log({
830
+ operation: 'create-branch',
831
+ operationType: 'CREATE',
832
+ componentType: 'Branch',
833
+ componentName: branchName,
834
+ success: false,
835
+ error: error.message,
836
+ parameters: { repoId, branchName, fromBranch },
837
+ executionTimeMs: timer(),
838
+ });
839
+ throw error;
840
+ }
841
+ }
842
+ /**
843
+ * Update file content (requires GHE_ENABLE_WRITE=true)
844
+ */
845
+ async updateFile(repoId, path, content, message, branch, sha) {
846
+ if (!this.config.enableWrite) {
847
+ throw new Error('File updates are disabled. Set GHE_ENABLE_WRITE=true to enable.');
848
+ }
849
+ const timer = auditLogger.startTimer();
850
+ const repo = this.getRepoById(repoId);
851
+ try {
852
+ const encodedContent = Buffer.from(content).toString('base64');
853
+ const result = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/contents/${path}`, {
854
+ method: 'PUT',
855
+ data: {
856
+ message,
857
+ content: encodedContent,
858
+ sha,
859
+ branch,
860
+ },
861
+ useCache: false,
862
+ });
863
+ auditLogger.log({
864
+ operation: 'update-file',
865
+ operationType: 'UPDATE',
866
+ componentType: 'File',
867
+ componentName: path,
868
+ success: true,
869
+ parameters: { repoId, path, branch, message },
870
+ executionTimeMs: timer(),
871
+ });
872
+ return result;
873
+ }
874
+ catch (error) {
875
+ auditLogger.log({
876
+ operation: 'update-file',
877
+ operationType: 'UPDATE',
878
+ componentType: 'File',
879
+ componentName: path,
880
+ success: false,
881
+ error: error.message,
882
+ parameters: { repoId, path, branch },
883
+ executionTimeMs: timer(),
884
+ });
885
+ throw error;
886
+ }
887
+ }
888
+ /**
889
+ * Create a new file (requires GHE_ENABLE_CREATE=true)
890
+ */
891
+ async createFile(repoId, path, content, message, branch) {
892
+ if (!this.config.enableCreate) {
893
+ throw new Error('File creation is disabled. Set GHE_ENABLE_CREATE=true to enable.');
894
+ }
895
+ const timer = auditLogger.startTimer();
896
+ const repo = this.getRepoById(repoId);
897
+ try {
898
+ const encodedContent = Buffer.from(content).toString('base64');
899
+ const result = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/contents/${path}`, {
900
+ method: 'PUT',
901
+ data: {
902
+ message,
903
+ content: encodedContent,
904
+ branch,
905
+ },
906
+ useCache: false,
907
+ });
908
+ auditLogger.log({
909
+ operation: 'create-file',
910
+ operationType: 'CREATE',
911
+ componentType: 'File',
912
+ componentName: path,
913
+ success: true,
914
+ parameters: { repoId, path, branch, message },
915
+ executionTimeMs: timer(),
916
+ });
917
+ return result;
918
+ }
919
+ catch (error) {
920
+ auditLogger.log({
921
+ operation: 'create-file',
922
+ operationType: 'CREATE',
923
+ componentType: 'File',
924
+ componentName: path,
925
+ success: false,
926
+ error: error.message,
927
+ parameters: { repoId, path, branch },
928
+ executionTimeMs: timer(),
929
+ });
930
+ throw error;
931
+ }
932
+ }
933
+ /**
934
+ * Search repositories
935
+ */
936
+ async searchRepositories(query, owner) {
937
+ const timer = auditLogger.startTimer();
938
+ try {
939
+ let searchQuery = query;
940
+ if (owner) {
941
+ searchQuery += ` org:${owner}`;
942
+ }
943
+ const result = await this.makeRequest(`search/repositories?q=${encodeURIComponent(searchQuery)}`, { useCache: false });
944
+ auditLogger.log({
945
+ operation: 'search-repositories',
946
+ operationType: 'READ',
947
+ componentType: 'Repository',
948
+ success: true,
949
+ parameters: { query, owner, totalResults: result.total_count },
950
+ executionTimeMs: timer(),
951
+ });
952
+ return result;
953
+ }
954
+ catch (error) {
955
+ auditLogger.log({
956
+ operation: 'search-repositories',
957
+ operationType: 'READ',
958
+ componentType: 'Repository',
959
+ success: false,
960
+ error: error.message,
961
+ parameters: { query, owner },
962
+ executionTimeMs: timer(),
963
+ });
964
+ throw error;
965
+ }
966
+ }
967
+ /**
968
+ * Get directory structure recursively
969
+ */
970
+ async getDirectoryStructure(repoId, path, branch, depth = 3) {
971
+ const timer = auditLogger.startTimer();
972
+ const repo = this.getRepoById(repoId);
973
+ try {
974
+ // Auto-detect branch if not specified
975
+ const selectedBranch = branch || (await this.getDefaultBranch(repoId)).branch;
976
+ // Recursive function to build tree
977
+ const buildTree = async (currentPath, currentDepth) => {
978
+ if (currentDepth > depth) {
979
+ return { truncated: true };
980
+ }
981
+ const contents = await this.makeRequest(`repos/${repo.owner}/${repo.repo}/contents/${currentPath}?ref=${selectedBranch}`, { repoId });
982
+ const tree = [];
983
+ for (const item of contents) {
984
+ if (item.type === 'dir' && currentDepth < depth) {
985
+ tree.push({
986
+ ...item,
987
+ children: await buildTree(item.path, currentDepth + 1)
988
+ });
989
+ }
990
+ else {
991
+ tree.push(item);
992
+ }
993
+ }
994
+ return tree;
995
+ };
996
+ const tree = await buildTree(path || '', 1);
997
+ auditLogger.log({
998
+ operation: 'get-directory-structure',
999
+ operationType: 'READ',
1000
+ componentType: 'Directory',
1001
+ componentName: path || '/',
1002
+ success: true,
1003
+ parameters: { repoId, path, branch: selectedBranch, depth },
1004
+ executionTimeMs: timer(),
1005
+ });
1006
+ return { tree, branch: selectedBranch };
1007
+ }
1008
+ catch (error) {
1009
+ auditLogger.log({
1010
+ operation: 'get-directory-structure',
1011
+ operationType: 'READ',
1012
+ componentType: 'Directory',
1013
+ componentName: path || '/',
1014
+ success: false,
1015
+ error: error.message,
1016
+ parameters: { repoId, path, branch, depth },
1017
+ executionTimeMs: timer(),
1018
+ });
1019
+ throw error;
1020
+ }
1021
+ }
1022
+ /**
1023
+ * Get file commit history
1024
+ */
1025
+ async getFileHistory(repoId, path, branch, limit = 50) {
1026
+ const timer = auditLogger.startTimer();
1027
+ try {
1028
+ const commits = await this.getCommits(repoId, branch, undefined, undefined, undefined, path, limit);
1029
+ auditLogger.log({
1030
+ operation: 'get-file-history',
1031
+ operationType: 'READ',
1032
+ componentType: 'File',
1033
+ componentName: path,
1034
+ success: true,
1035
+ parameters: { repoId, path, branch, limit, count: commits.length },
1036
+ executionTimeMs: timer(),
1037
+ });
1038
+ return commits;
1039
+ }
1040
+ catch (error) {
1041
+ auditLogger.log({
1042
+ operation: 'get-file-history',
1043
+ operationType: 'READ',
1044
+ componentType: 'File',
1045
+ componentName: path,
1046
+ success: false,
1047
+ error: error.message,
1048
+ parameters: { repoId, path, branch, limit },
1049
+ executionTimeMs: timer(),
1050
+ });
1051
+ throw error;
1052
+ }
1053
+ }
1054
+ /**
1055
+ * Get commit diff
1056
+ */
1057
+ async getCommitDiff(repoId, sha, format = 'diff') {
1058
+ const timer = auditLogger.startTimer();
1059
+ const repo = this.getRepoById(repoId);
1060
+ try {
1061
+ const acceptHeader = format === 'patch'
1062
+ ? 'application/vnd.github.v3.patch'
1063
+ : 'application/vnd.github.v3.diff';
1064
+ const token = await this.getAccessToken();
1065
+ const url = `${this.baseApiUrl}/repos/${repo.owner}/${repo.repo}/commits/${sha}`;
1066
+ const response = await axios({
1067
+ method: 'GET',
1068
+ url,
1069
+ headers: {
1070
+ 'Authorization': `token ${token}`,
1071
+ 'Accept': acceptHeader,
1072
+ 'X-GitHub-Api-Version': this.config.apiVersion,
1073
+ },
1074
+ });
1075
+ auditLogger.log({
1076
+ operation: 'get-commit-diff',
1077
+ operationType: 'READ',
1078
+ componentType: 'Commit',
1079
+ componentId: sha,
1080
+ success: true,
1081
+ parameters: { repoId, sha, format },
1082
+ executionTimeMs: timer(),
1083
+ });
1084
+ return response.data;
1085
+ }
1086
+ catch (error) {
1087
+ auditLogger.log({
1088
+ operation: 'get-commit-diff',
1089
+ operationType: 'READ',
1090
+ componentType: 'Commit',
1091
+ componentId: sha,
1092
+ success: false,
1093
+ error: error.message,
1094
+ parameters: { repoId, sha, format },
1095
+ executionTimeMs: timer(),
1096
+ });
1097
+ throw error;
1098
+ }
1099
+ }
1100
+ }
1101
+ //# sourceMappingURL=GitHubEnterpriseService.js.map