gh-load-pull-request 0.6.0 → 0.7.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.
package/README.md CHANGED
@@ -73,10 +73,45 @@ chmod +x gh-load-pull-request.mjs
73
73
  Usage: gh-load-pull-request <pr-url> [options]
74
74
 
75
75
  Options:
76
- -t, --token GitHub personal access token (optional for public PRs)
77
- -o, --output Output file path (default: stdout)
78
- -h, --help Show help
79
- --version Show version number
76
+ -t, --token GitHub personal access token (optional for public PRs)
77
+ -o, --output Output directory (creates pr-<number>/ subfolder)
78
+ --format Output format: markdown, json (default: markdown)
79
+ --download-images Download embedded images (default: true)
80
+ --include-reviews Include PR reviews (default: true)
81
+ --force-api Force using GitHub API instead of gh CLI
82
+ --force-gh Force using gh CLI, fail if not available
83
+ -v, --verbose Enable verbose logging
84
+ -h, --help Show help
85
+ --version Show version number
86
+ ```
87
+
88
+ ## Backend Modes
89
+
90
+ The tool supports two backend modes for fetching PR data:
91
+
92
+ ### 1. gh CLI Mode (Default)
93
+
94
+ By default, the tool uses the [GitHub CLI](https://cli.github.com/) (`gh`) to fetch PR data. This is the recommended mode as it:
95
+
96
+ - Uses your existing `gh` authentication
97
+ - Doesn't require managing tokens separately
98
+ - Works seamlessly with GitHub Enterprise
99
+
100
+ ### 2. API Mode (Fallback)
101
+
102
+ If `gh` CLI is not available or not authenticated, the tool automatically falls back to using the GitHub REST API via Octokit. You can also force this mode with `--force-api`.
103
+
104
+ ### Controlling Backend Mode
105
+
106
+ ```bash
107
+ # Use default mode (gh CLI with API fallback)
108
+ gh-load-pull-request owner/repo#123
109
+
110
+ # Force gh CLI mode (fails if gh is not available)
111
+ gh-load-pull-request owner/repo#123 --force-gh
112
+
113
+ # Force API mode (useful for testing or when gh has issues)
114
+ gh-load-pull-request owner/repo#123 --force-api
80
115
  ```
81
116
 
82
117
  ## Input Formats
@@ -152,7 +187,7 @@ gh-load-pull-request https://github.com/facebook/react/pull/28000
152
187
  gh-load-pull-request facebook/react#28000
153
188
 
154
189
  # Save to file
155
- gh-load-pull-request facebook/react#28000 -o react-pr-28000.md
190
+ gh-load-pull-request facebook/react#28000 -o ./output
156
191
 
157
192
  # Download private PR using gh CLI auth
158
193
  gh-load-pull-request myorg/private-repo#42
@@ -160,6 +195,15 @@ gh-load-pull-request myorg/private-repo#42
160
195
  # Download with explicit token
161
196
  gh-load-pull-request myorg/repo#123 --token ghp_your_token_here
162
197
 
198
+ # Force using GitHub API instead of gh CLI
199
+ gh-load-pull-request owner/repo#123 --force-api
200
+
201
+ # Output as JSON
202
+ gh-load-pull-request owner/repo#123 --format json
203
+
204
+ # Verbose mode for debugging
205
+ gh-load-pull-request owner/repo#123 -v
206
+
163
207
  # Pipe to other tools (e.g., AI for review)
164
208
  gh-load-pull-request owner/repo#123 | claude-analyze
165
209
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gh-load-pull-request",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Download GitHub pull request and convert it to markdown",
5
5
  "type": "module",
6
6
  "main": "src/gh-load-pull-request.mjs",
@@ -0,0 +1,495 @@
1
+ /**
2
+ * Backend implementations for gh-load-pull-request
3
+ * Contains functions for fetching PR data via gh CLI and GitHub API
4
+ */
5
+
6
+ import { Octokit } from '@octokit/rest';
7
+
8
+ let verboseLog = () => {};
9
+ let log = () => {};
10
+
11
+ /**
12
+ * Set logging functions from parent module
13
+ * @param {Object} loggers - Object with log and verboseLog functions
14
+ */
15
+ export function setLoggers(loggers) {
16
+ log = loggers.log || (() => {});
17
+ verboseLog = loggers.verboseLog || (() => {});
18
+ }
19
+
20
+ /**
21
+ * Check if gh CLI is installed and available
22
+ * @returns {Promise<boolean>} True if gh is installed
23
+ */
24
+ export async function isGhInstalled() {
25
+ try {
26
+ const { execSync } = await import('node:child_process');
27
+ execSync('gh --version', { stdio: 'pipe' });
28
+ return true;
29
+ } catch (_error) {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Check if gh CLI is authenticated
36
+ * @returns {Promise<boolean>} True if gh is authenticated
37
+ */
38
+ export async function isGhAuthenticated() {
39
+ try {
40
+ const { execSync } = await import('node:child_process');
41
+ execSync('gh auth status', { stdio: 'pipe' });
42
+ return true;
43
+ } catch (_error) {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Get GitHub token from gh CLI if available
50
+ * @returns {Promise<string|null>} GitHub token or null
51
+ */
52
+ export async function getGhToken() {
53
+ try {
54
+ if (!(await isGhInstalled())) {
55
+ return null;
56
+ }
57
+
58
+ const { execSync } = await import('node:child_process');
59
+ const token = execSync('gh auth token', {
60
+ encoding: 'utf8',
61
+ stdio: 'pipe',
62
+ }).trim();
63
+ return token;
64
+ } catch (_error) {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Transform gh CLI JSON output to match the expected PR data format
71
+ * @param {Object} ghData - Data from gh pr view --json
72
+ * @param {string} owner - Repository owner
73
+ * @param {string} repo - Repository name
74
+ * @returns {Object} Transformed PR data matching Octokit format
75
+ */
76
+ function transformGhPrData(ghData, owner, repo) {
77
+ // Transform PR object to match Octokit format
78
+ const pr = {
79
+ number: ghData.number,
80
+ title: ghData.title,
81
+ state: ghData.state.toLowerCase(),
82
+ draft: ghData.isDraft,
83
+ merged: ghData.state === 'MERGED',
84
+ html_url: ghData.url,
85
+ user: {
86
+ login: ghData.author.login,
87
+ },
88
+ created_at: ghData.createdAt,
89
+ updated_at: ghData.updatedAt,
90
+ merged_at: ghData.mergedAt,
91
+ closed_at: ghData.closedAt,
92
+ merged_by: ghData.mergedBy ? { login: ghData.mergedBy.login } : null,
93
+ base: {
94
+ ref: ghData.baseRefName,
95
+ sha: ghData.baseRefOid,
96
+ },
97
+ head: {
98
+ ref: ghData.headRefName,
99
+ sha: ghData.headRefOid,
100
+ },
101
+ additions: ghData.additions,
102
+ deletions: ghData.deletions,
103
+ changed_files: ghData.changedFiles,
104
+ labels:
105
+ ghData.labels?.map((l) => ({ name: l.name, color: l.color || '' })) || [],
106
+ assignees: ghData.assignees?.map((a) => ({ login: a.login })) || [],
107
+ requested_reviewers:
108
+ ghData.reviewRequests?.map((r) => ({ login: r.login })) || [],
109
+ milestone: ghData.milestone
110
+ ? { title: ghData.milestone.title, number: ghData.milestone.number }
111
+ : null,
112
+ body: ghData.body,
113
+ };
114
+
115
+ // Transform files
116
+ const files = (ghData.files || []).map((f) => ({
117
+ filename: f.path,
118
+ status:
119
+ f.additions > 0 && f.deletions === 0
120
+ ? 'added'
121
+ : f.additions === 0 && f.deletions > 0
122
+ ? 'removed'
123
+ : 'modified',
124
+ additions: f.additions,
125
+ deletions: f.deletions,
126
+ previous_filename: null, // gh CLI doesn't provide this in the same way
127
+ patch: '', // gh CLI doesn't include patch in pr view
128
+ }));
129
+
130
+ // Transform commits
131
+ const commits = (ghData.commits || []).map((c) => ({
132
+ sha: c.oid,
133
+ commit: {
134
+ message: `${c.messageHeadline}\n\n${c.messageBody || ''}`.trim(),
135
+ author: {
136
+ name: c.authors?.[0]?.name || 'unknown',
137
+ date: c.authoredDate,
138
+ },
139
+ },
140
+ html_url: `https://github.com/${owner}/${repo}/commit/${c.oid}`,
141
+ author: c.authors?.[0]?.login ? { login: c.authors[0].login } : null,
142
+ }));
143
+
144
+ // Transform issue comments
145
+ const comments = (ghData.comments || []).map((c) => ({
146
+ id: c.id,
147
+ user: { login: c.author?.login || 'unknown' },
148
+ body: c.body,
149
+ created_at: c.createdAt,
150
+ }));
151
+
152
+ // Transform reviews
153
+ const reviews = (ghData.reviews || []).map((r) => ({
154
+ id: r.id,
155
+ user: { login: r.author?.login || 'unknown' },
156
+ state: r.state,
157
+ body: r.body,
158
+ submitted_at: r.submittedAt,
159
+ }));
160
+
161
+ // Review comments need to be fetched separately via gh api
162
+ // as gh pr view doesn't include them in a usable format
163
+ const reviewComments = [];
164
+
165
+ return {
166
+ pr,
167
+ files,
168
+ comments,
169
+ reviewComments,
170
+ reviews,
171
+ commits,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Fetch pull request data using gh CLI
177
+ * @param {Object} options - Options for fetching PR
178
+ * @param {string} options.owner - Repository owner
179
+ * @param {string} options.repo - Repository name
180
+ * @param {number} options.prNumber - Pull request number
181
+ * @param {boolean} options.includeReviews - Include PR reviews (default: true)
182
+ * @returns {Promise<Object>} PR data object with pr, files, comments, reviewComments, reviews, commits
183
+ */
184
+ export async function loadPullRequestWithGh(options) {
185
+ const { owner, repo, prNumber, includeReviews = true } = options;
186
+
187
+ try {
188
+ log(
189
+ 'blue',
190
+ `🔍 Fetching pull request ${owner}/${repo}#${prNumber} using gh CLI...`
191
+ );
192
+
193
+ const { execSync } = await import('node:child_process');
194
+
195
+ // Build the list of JSON fields to fetch
196
+ const jsonFields = [
197
+ 'number',
198
+ 'title',
199
+ 'state',
200
+ 'isDraft',
201
+ 'body',
202
+ 'url',
203
+ 'author',
204
+ 'createdAt',
205
+ 'updatedAt',
206
+ 'mergedAt',
207
+ 'closedAt',
208
+ 'mergedBy',
209
+ 'baseRefName',
210
+ 'baseRefOid',
211
+ 'headRefName',
212
+ 'headRefOid',
213
+ 'additions',
214
+ 'deletions',
215
+ 'changedFiles',
216
+ 'labels',
217
+ 'assignees',
218
+ 'reviewRequests',
219
+ 'milestone',
220
+ 'files',
221
+ 'commits',
222
+ 'comments',
223
+ ];
224
+
225
+ if (includeReviews) {
226
+ jsonFields.push('reviews');
227
+ }
228
+
229
+ // Fetch PR data using gh pr view
230
+ const ghCommand = `gh pr view ${prNumber} --repo ${owner}/${repo} --json ${jsonFields.join(',')}`;
231
+ verboseLog('dim', ` Running: ${ghCommand}`);
232
+
233
+ const ghOutput = execSync(ghCommand, {
234
+ encoding: 'utf8',
235
+ stdio: 'pipe',
236
+ timeout: 60000,
237
+ });
238
+
239
+ const ghData = JSON.parse(ghOutput);
240
+
241
+ // Fetch review comments separately via gh api
242
+ let reviewComments = [];
243
+ try {
244
+ const reviewCommentsCommand = `gh api repos/${owner}/${repo}/pulls/${prNumber}/comments`;
245
+ verboseLog('dim', ` Running: ${reviewCommentsCommand}`);
246
+
247
+ const reviewCommentsOutput = execSync(reviewCommentsCommand, {
248
+ encoding: 'utf8',
249
+ stdio: 'pipe',
250
+ timeout: 30000,
251
+ });
252
+
253
+ const rawReviewComments = JSON.parse(reviewCommentsOutput);
254
+ reviewComments = rawReviewComments.map((c) => ({
255
+ id: c.id,
256
+ user: { login: c.user?.login || 'unknown' },
257
+ body: c.body,
258
+ path: c.path,
259
+ line: c.line,
260
+ created_at: c.created_at,
261
+ diff_hunk: c.diff_hunk,
262
+ pull_request_review_id: c.pull_request_review_id,
263
+ }));
264
+ } catch (reviewError) {
265
+ verboseLog(
266
+ 'yellow',
267
+ ` ⚠️ Could not fetch review comments: ${reviewError.message}`
268
+ );
269
+ }
270
+
271
+ // Transform gh data to expected format
272
+ const transformedData = transformGhPrData(ghData, owner, repo);
273
+ transformedData.reviewComments = reviewComments;
274
+
275
+ log('green', `✅ Successfully fetched PR data using gh CLI`);
276
+
277
+ return transformedData;
278
+ } catch (error) {
279
+ const errorMessage = error.message || String(error);
280
+ if (
281
+ errorMessage.includes('not found') ||
282
+ errorMessage.includes('Could not resolve')
283
+ ) {
284
+ throw new Error(`Pull request not found: ${owner}/${repo}#${prNumber}`);
285
+ } else if (errorMessage.includes('auth') || errorMessage.includes('401')) {
286
+ throw new Error(
287
+ 'Authentication failed. Please run "gh auth login" to authenticate'
288
+ );
289
+ } else {
290
+ throw new Error(
291
+ `Failed to fetch pull request via gh CLI: ${errorMessage}`
292
+ );
293
+ }
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Fetch pull request data using GitHub REST API via Octokit
299
+ * @param {Object} options - Options for fetching PR
300
+ * @param {string} options.owner - Repository owner
301
+ * @param {string} options.repo - Repository name
302
+ * @param {number} options.prNumber - Pull request number
303
+ * @param {string} options.token - GitHub token (optional for public repos)
304
+ * @param {boolean} options.includeReviews - Include PR reviews (default: true)
305
+ * @returns {Promise<Object>} PR data object with pr, files, comments, reviewComments, reviews, commits
306
+ */
307
+ export async function loadPullRequestWithApi(options) {
308
+ const { owner, repo, prNumber, token, includeReviews = true } = options;
309
+
310
+ try {
311
+ log(
312
+ 'blue',
313
+ `🔍 Fetching pull request ${owner}/${repo}#${prNumber} using API...`
314
+ );
315
+
316
+ const octokit = new Octokit({
317
+ auth: token,
318
+ baseUrl: 'https://api.github.com',
319
+ });
320
+
321
+ // Fetch PR data
322
+ const { data: pr } = await octokit.rest.pulls.get({
323
+ owner,
324
+ repo,
325
+ pull_number: prNumber,
326
+ });
327
+
328
+ // Fetch PR files
329
+ const { data: files } = await octokit.rest.pulls.listFiles({
330
+ owner,
331
+ repo,
332
+ pull_number: prNumber,
333
+ });
334
+
335
+ // Fetch PR comments (issue comments)
336
+ const { data: comments } = await octokit.rest.issues.listComments({
337
+ owner,
338
+ repo,
339
+ issue_number: prNumber,
340
+ });
341
+
342
+ // Fetch PR review comments (inline code comments)
343
+ const { data: reviewComments } =
344
+ await octokit.rest.pulls.listReviewComments({
345
+ owner,
346
+ repo,
347
+ pull_number: prNumber,
348
+ });
349
+
350
+ // Fetch PR reviews
351
+ let reviews = [];
352
+ if (includeReviews) {
353
+ const { data: reviewsData } = await octokit.rest.pulls.listReviews({
354
+ owner,
355
+ repo,
356
+ pull_number: prNumber,
357
+ });
358
+ reviews = reviewsData;
359
+ }
360
+
361
+ // Fetch PR commits
362
+ const { data: commits } = await octokit.rest.pulls.listCommits({
363
+ owner,
364
+ repo,
365
+ pull_number: prNumber,
366
+ });
367
+
368
+ log('green', `✅ Successfully fetched PR data using API`);
369
+
370
+ return {
371
+ pr,
372
+ files,
373
+ comments,
374
+ reviewComments,
375
+ reviews,
376
+ commits,
377
+ };
378
+ } catch (error) {
379
+ if (error.status === 404) {
380
+ throw new Error(`Pull request not found: ${owner}/${repo}#${prNumber}`);
381
+ } else if (error.status === 401) {
382
+ throw new Error(
383
+ 'Authentication failed. Please provide a valid GitHub token'
384
+ );
385
+ } else {
386
+ throw new Error(`Failed to fetch pull request via API: ${error.message}`);
387
+ }
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Fetch pull request data from GitHub
393
+ * By default uses gh CLI if available, falls back to API otherwise.
394
+ * @param {Object} options - Options for fetching PR
395
+ * @param {string} options.owner - Repository owner
396
+ * @param {string} options.repo - Repository name
397
+ * @param {number} options.prNumber - Pull request number
398
+ * @param {string} options.token - GitHub token (optional for public repos, used for API fallback)
399
+ * @param {boolean} options.includeReviews - Include PR reviews (default: true)
400
+ * @param {boolean} options.forceApi - Force using API instead of gh CLI (default: false)
401
+ * @param {boolean} options.forceGh - Force using gh CLI, fail if not available (default: false)
402
+ * @returns {Promise<Object>} PR data object with pr, files, comments, reviewComments, reviews, commits
403
+ */
404
+ export async function loadPullRequest(options) {
405
+ const {
406
+ owner,
407
+ repo,
408
+ prNumber,
409
+ token,
410
+ includeReviews = true,
411
+ forceApi = false,
412
+ forceGh = false,
413
+ } = options;
414
+
415
+ // If force API mode, use API directly
416
+ if (forceApi) {
417
+ verboseLog('cyan', '🔧 Using API mode (forced)');
418
+ return loadPullRequestWithApi({
419
+ owner,
420
+ repo,
421
+ prNumber,
422
+ token,
423
+ includeReviews,
424
+ });
425
+ }
426
+
427
+ // Check if gh CLI is available
428
+ const ghInstalled = await isGhInstalled();
429
+
430
+ // If force gh mode but gh is not available, throw error
431
+ if (forceGh && !ghInstalled) {
432
+ throw new Error(
433
+ 'gh CLI is required but not installed. Please install GitHub CLI: https://cli.github.com/'
434
+ );
435
+ }
436
+
437
+ // If gh is available, try using it first
438
+ if (ghInstalled) {
439
+ // Check if authenticated
440
+ const ghAuth = await isGhAuthenticated();
441
+ if (!ghAuth) {
442
+ verboseLog(
443
+ 'yellow',
444
+ '⚠️ gh CLI is not authenticated, falling back to API'
445
+ );
446
+ if (forceGh) {
447
+ throw new Error(
448
+ 'gh CLI is not authenticated. Please run "gh auth login"'
449
+ );
450
+ }
451
+ return loadPullRequestWithApi({
452
+ owner,
453
+ repo,
454
+ prNumber,
455
+ token,
456
+ includeReviews,
457
+ });
458
+ }
459
+
460
+ try {
461
+ return await loadPullRequestWithGh({
462
+ owner,
463
+ repo,
464
+ prNumber,
465
+ includeReviews,
466
+ });
467
+ } catch (ghError) {
468
+ // If gh fails and we're not forcing gh, fall back to API
469
+ if (!forceGh) {
470
+ verboseLog(
471
+ 'yellow',
472
+ `⚠️ gh CLI failed: ${ghError.message}, falling back to API`
473
+ );
474
+ return loadPullRequestWithApi({
475
+ owner,
476
+ repo,
477
+ prNumber,
478
+ token,
479
+ includeReviews,
480
+ });
481
+ }
482
+ throw ghError;
483
+ }
484
+ }
485
+
486
+ // gh not available, use API
487
+ verboseLog('cyan', '🔧 gh CLI not available, using API');
488
+ return loadPullRequestWithApi({
489
+ owner,
490
+ repo,
491
+ prNumber,
492
+ token,
493
+ includeReviews,
494
+ });
495
+ }
@@ -8,7 +8,6 @@ import http from 'node:http';
8
8
  const __filename = fileURLToPath(import.meta.url);
9
9
  const __dirname = path.dirname(__filename);
10
10
 
11
- import { Octokit } from '@octokit/rest';
12
11
  import fs from 'fs-extra';
13
12
  import yargs from 'yargs';
14
13
  import { hideBin } from 'yargs/helpers';
@@ -21,6 +20,16 @@ import {
21
20
  generateFilesMarkdown,
22
21
  } from './formatters.mjs';
23
22
 
23
+ import {
24
+ isGhInstalled as backendsIsGhInstalled,
25
+ isGhAuthenticated as backendsIsGhAuthenticated,
26
+ getGhToken as backendsGetGhToken,
27
+ loadPullRequest as backendsLoadPullRequest,
28
+ loadPullRequestWithGh as backendsLoadPullRequestWithGh,
29
+ loadPullRequestWithApi as backendsLoadPullRequestWithApi,
30
+ setLoggers,
31
+ } from './backends.mjs';
32
+
24
33
  let version = '0.1.0';
25
34
  try {
26
35
  const packagePath = path.join(__dirname, '..', 'package.json');
@@ -60,6 +69,9 @@ const verboseLog = (color, message) => {
60
69
  }
61
70
  };
62
71
 
72
+ // Initialize loggers for backends module
73
+ setLoggers({ log, verboseLog });
74
+
63
75
  /**
64
76
  * Set logging mode for library usage
65
77
  * @param {Object} options - Logging options
@@ -69,38 +81,17 @@ const verboseLog = (color, message) => {
69
81
  export function setLoggingMode(options = {}) {
70
82
  verboseMode = options.verbose || false;
71
83
  silentMode = options.silent || false;
84
+ // Update loggers in backends module
85
+ setLoggers({ log, verboseLog });
72
86
  }
73
87
 
74
- async function isGhInstalled() {
75
- try {
76
- const { execSync } = await import('node:child_process');
77
- execSync('gh --version', { stdio: 'pipe' });
78
- return true;
79
- } catch (_error) {
80
- return false;
81
- }
82
- }
83
-
84
- /**
85
- * Get GitHub token from gh CLI if available
86
- * @returns {Promise<string|null>} GitHub token or null
87
- */
88
- export async function getGhToken() {
89
- try {
90
- if (!(await isGhInstalled())) {
91
- return null;
92
- }
93
-
94
- const { execSync } = await import('node:child_process');
95
- const token = execSync('gh auth token', {
96
- encoding: 'utf8',
97
- stdio: 'pipe',
98
- }).trim();
99
- return token;
100
- } catch (_error) {
101
- return null;
102
- }
103
- }
88
+ // Re-export backend functions
89
+ export const isGhInstalled = backendsIsGhInstalled;
90
+ export const isGhAuthenticated = backendsIsGhAuthenticated;
91
+ export const getGhToken = backendsGetGhToken;
92
+ export const loadPullRequest = backendsLoadPullRequest;
93
+ export const loadPullRequestWithGh = backendsLoadPullRequestWithGh;
94
+ export const loadPullRequestWithApi = backendsLoadPullRequestWithApi;
104
95
 
105
96
  /**
106
97
  * Parse PR URL to extract owner, repo, and PR number
@@ -404,97 +395,6 @@ export async function downloadImages(content, imagesDir, token, _prNumber) {
404
395
  return { content: updatedContent, downloadedImages };
405
396
  }
406
397
 
407
- /**
408
- * Fetch pull request data from GitHub API
409
- * @param {Object} options - Options for fetching PR
410
- * @param {string} options.owner - Repository owner
411
- * @param {string} options.repo - Repository name
412
- * @param {number} options.prNumber - Pull request number
413
- * @param {string} options.token - GitHub token (optional for public repos)
414
- * @param {boolean} options.includeReviews - Include PR reviews (default: true)
415
- * @returns {Promise<Object>} PR data object with pr, files, comments, reviewComments, reviews, commits
416
- */
417
- export async function loadPullRequest(options) {
418
- const { owner, repo, prNumber, token, includeReviews = true } = options;
419
-
420
- try {
421
- log('blue', `🔍 Fetching pull request ${owner}/${repo}#${prNumber}...`);
422
-
423
- const octokit = new Octokit({
424
- auth: token,
425
- baseUrl: 'https://api.github.com',
426
- });
427
-
428
- // Fetch PR data
429
- const { data: pr } = await octokit.rest.pulls.get({
430
- owner,
431
- repo,
432
- pull_number: prNumber,
433
- });
434
-
435
- // Fetch PR files
436
- const { data: files } = await octokit.rest.pulls.listFiles({
437
- owner,
438
- repo,
439
- pull_number: prNumber,
440
- });
441
-
442
- // Fetch PR comments (issue comments)
443
- const { data: comments } = await octokit.rest.issues.listComments({
444
- owner,
445
- repo,
446
- issue_number: prNumber,
447
- });
448
-
449
- // Fetch PR review comments (inline code comments)
450
- const { data: reviewComments } =
451
- await octokit.rest.pulls.listReviewComments({
452
- owner,
453
- repo,
454
- pull_number: prNumber,
455
- });
456
-
457
- // Fetch PR reviews
458
- let reviews = [];
459
- if (includeReviews) {
460
- const { data: reviewsData } = await octokit.rest.pulls.listReviews({
461
- owner,
462
- repo,
463
- pull_number: prNumber,
464
- });
465
- reviews = reviewsData;
466
- }
467
-
468
- // Fetch PR commits
469
- const { data: commits } = await octokit.rest.pulls.listCommits({
470
- owner,
471
- repo,
472
- pull_number: prNumber,
473
- });
474
-
475
- log('green', `✅ Successfully fetched PR data`);
476
-
477
- return {
478
- pr,
479
- files,
480
- comments,
481
- reviewComments,
482
- reviews,
483
- commits,
484
- };
485
- } catch (error) {
486
- if (error.status === 404) {
487
- throw new Error(`Pull request not found: ${owner}/${repo}#${prNumber}`);
488
- } else if (error.status === 401) {
489
- throw new Error(
490
- 'Authentication failed. Please provide a valid GitHub token'
491
- );
492
- } else {
493
- throw new Error(`Failed to fetch pull request: ${error.message}`);
494
- }
495
- }
496
- }
497
-
498
398
  // Alias for backwards compatibility
499
399
  export const fetchPullRequest = (owner, repo, prNumber, token, options = {}) =>
500
400
  loadPullRequest({ owner, repo, prNumber, token, ...options });
@@ -888,6 +788,16 @@ function parseCliArgs() {
888
788
  describe: 'Enable verbose logging',
889
789
  default: false,
890
790
  })
791
+ .option('force-api', {
792
+ type: 'boolean',
793
+ describe: 'Force using GitHub API instead of gh CLI',
794
+ default: false,
795
+ })
796
+ .option('force-gh', {
797
+ type: 'boolean',
798
+ describe: 'Force using gh CLI, fail if not available',
799
+ default: false,
800
+ })
891
801
  .help('h')
892
802
  .alias('h', 'help')
893
803
  .example('$0 https://github.com/owner/repo/pull/123', 'Download PR #123')
@@ -898,7 +808,9 @@ function parseCliArgs() {
898
808
  .example(
899
809
  '$0 https://github.com/owner/repo/pull/123 --token ghp_xxx',
900
810
  'Download private PR'
901
- ).argv;
811
+ )
812
+ .example('$0 owner/repo#123 --force-api', 'Force using GitHub API')
813
+ .example('$0 owner/repo#123 --force-gh', 'Force using gh CLI').argv;
902
814
  }
903
815
 
904
816
  async function main() {
@@ -911,11 +823,22 @@ async function main() {
911
823
  'include-reviews': includeReviews,
912
824
  format,
913
825
  verbose,
826
+ 'force-api': forceApi,
827
+ 'force-gh': forceGh,
914
828
  } = argv;
915
829
 
916
830
  // Set verbose mode
917
831
  verboseMode = verbose;
918
832
 
833
+ // Validate mutually exclusive flags
834
+ if (forceApi && forceGh) {
835
+ log(
836
+ 'red',
837
+ '❌ Cannot use both --force-api and --force-gh at the same time'
838
+ );
839
+ process.exit(1);
840
+ }
841
+
919
842
  // Parse PR input first (before potentially slow gh CLI token fetch)
920
843
  const prInfo = parsePrUrl(prInput);
921
844
  if (!prInfo) {
@@ -929,12 +852,16 @@ async function main() {
929
852
 
930
853
  let token = tokenArg;
931
854
 
932
- // If no token provided, try to get it from gh CLI
933
- if (!token || token === undefined) {
855
+ // If force-api mode or no token provided, try to get token from gh CLI for API fallback
856
+ if (forceApi || !token || token === undefined) {
934
857
  const ghToken = await getGhToken();
935
858
  if (ghToken) {
936
859
  token = ghToken;
937
- log('cyan', '🔑 Using GitHub token from gh CLI');
860
+ if (forceApi) {
861
+ log('cyan', '🔑 Using GitHub token from gh CLI for API mode');
862
+ } else {
863
+ verboseLog('cyan', '🔑 Got GitHub token from gh CLI for fallback');
864
+ }
938
865
  }
939
866
  }
940
867
 
@@ -948,6 +875,8 @@ async function main() {
948
875
  prNumber,
949
876
  token,
950
877
  includeReviews,
878
+ forceApi,
879
+ forceGh,
951
880
  });
952
881
 
953
882
  // Determine output paths