gh-load-pull-request 0.5.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 +49 -5
- package/package.json +1 -1
- package/src/backends.mjs +495 -0
- package/src/formatters.mjs +245 -0
- package/src/gh-load-pull-request.mjs +477 -474
|
@@ -1,37 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
// Import built-in Node.js modules
|
|
4
3
|
import path from 'node:path';
|
|
5
4
|
import { fileURLToPath } from 'node:url';
|
|
6
5
|
import https from 'node:https';
|
|
7
6
|
import http from 'node:http';
|
|
8
7
|
|
|
9
|
-
// Get __dirname equivalent for ES modules
|
|
10
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
9
|
const __dirname = path.dirname(__filename);
|
|
12
10
|
|
|
13
|
-
// Import npm dependencies
|
|
14
|
-
import { Octokit } from '@octokit/rest';
|
|
15
11
|
import fs from 'fs-extra';
|
|
16
12
|
import yargs from 'yargs';
|
|
17
13
|
import { hideBin } from 'yargs/helpers';
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
import {
|
|
16
|
+
formatDate,
|
|
17
|
+
convertToJson as formattersConvertToJson,
|
|
18
|
+
generateMetadataMarkdown,
|
|
19
|
+
generateCommitsMarkdown,
|
|
20
|
+
generateFilesMarkdown,
|
|
21
|
+
} from './formatters.mjs';
|
|
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
|
+
|
|
33
|
+
let version = '0.1.0';
|
|
22
34
|
try {
|
|
23
|
-
const packagePath = path.join(__dirname, 'package.json');
|
|
24
|
-
// Use node:fs for Deno compatibility (fs-extra has issues with Deno)
|
|
35
|
+
const packagePath = path.join(__dirname, '..', 'package.json');
|
|
25
36
|
const { readFileSync, existsSync } = await import('node:fs');
|
|
26
37
|
if (existsSync(packagePath)) {
|
|
27
38
|
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
28
39
|
version = packageJson.version;
|
|
29
40
|
}
|
|
30
41
|
} catch (_error) {
|
|
31
|
-
|
|
42
|
+
/* Use fallback version */
|
|
32
43
|
}
|
|
33
44
|
|
|
34
|
-
// Colors for console output
|
|
35
45
|
const colors = {
|
|
36
46
|
green: '\x1b[32m',
|
|
37
47
|
yellow: '\x1b[33m',
|
|
@@ -44,55 +54,52 @@ const colors = {
|
|
|
44
54
|
reset: '\x1b[0m',
|
|
45
55
|
};
|
|
46
56
|
|
|
47
|
-
// Verbose logging flag (set by CLI option)
|
|
48
57
|
let verboseMode = false;
|
|
58
|
+
let silentMode = false;
|
|
49
59
|
|
|
50
|
-
const log = (color, message) =>
|
|
51
|
-
|
|
60
|
+
const log = (color, message) => {
|
|
61
|
+
if (!silentMode) {
|
|
62
|
+
console.error(`${colors[color]}${message}${colors.reset}`);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
52
65
|
|
|
53
66
|
const verboseLog = (color, message) => {
|
|
54
|
-
if (verboseMode) {
|
|
67
|
+
if (verboseMode && !silentMode) {
|
|
55
68
|
log(color, message);
|
|
56
69
|
}
|
|
57
70
|
};
|
|
58
71
|
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (!(await isGhInstalled())) {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const { execSync } = await import('node:child_process');
|
|
78
|
-
const token = execSync('gh auth token', {
|
|
79
|
-
encoding: 'utf8',
|
|
80
|
-
stdio: 'pipe',
|
|
81
|
-
}).trim();
|
|
82
|
-
return token;
|
|
83
|
-
} catch (_error) {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
72
|
+
// Initialize loggers for backends module
|
|
73
|
+
setLoggers({ log, verboseLog });
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Set logging mode for library usage
|
|
77
|
+
* @param {Object} options - Logging options
|
|
78
|
+
* @param {boolean} options.verbose - Enable verbose logging
|
|
79
|
+
* @param {boolean} options.silent - Disable all logging
|
|
80
|
+
*/
|
|
81
|
+
export function setLoggingMode(options = {}) {
|
|
82
|
+
verboseMode = options.verbose || false;
|
|
83
|
+
silentMode = options.silent || false;
|
|
84
|
+
// Update loggers in backends module
|
|
85
|
+
setLoggers({ log, verboseLog });
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parse PR URL to extract owner, repo, and PR number
|
|
98
|
+
* @param {string} url - PR URL or shorthand (owner/repo#123, owner/repo/123, or full URL)
|
|
99
|
+
* @returns {{owner: string, repo: string, prNumber: number}|null} Parsed PR info or null
|
|
100
|
+
*/
|
|
101
|
+
export function parsePrUrl(url) {
|
|
102
|
+
// Try full URL format (github.com/owner/repo/pull/123 or owner/repo#123 or owner/repo/123)
|
|
96
103
|
const urlMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
97
104
|
if (urlMatch) {
|
|
98
105
|
return {
|
|
@@ -125,7 +132,6 @@ function parsePrUrl(url) {
|
|
|
125
132
|
return null;
|
|
126
133
|
}
|
|
127
134
|
|
|
128
|
-
// Image magic bytes for validation
|
|
129
135
|
const imageMagicBytes = {
|
|
130
136
|
png: [0x89, 0x50, 0x4e, 0x47],
|
|
131
137
|
jpg: [0xff, 0xd8, 0xff],
|
|
@@ -136,8 +142,13 @@ const imageMagicBytes = {
|
|
|
136
142
|
svg: [0x3c, 0x3f, 0x78, 0x6d, 0x6c], // <?xml for SVG (though SVG can also start with <svg)
|
|
137
143
|
};
|
|
138
144
|
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Validate image by checking magic bytes
|
|
147
|
+
* @param {Buffer} buffer - Image buffer to validate
|
|
148
|
+
* @param {string} url - Original URL (for logging)
|
|
149
|
+
* @returns {{valid: boolean, format?: string, reason?: string}} Validation result
|
|
150
|
+
*/
|
|
151
|
+
export function validateImageBuffer(buffer, url) {
|
|
141
152
|
if (!buffer || buffer.length < 4) {
|
|
142
153
|
return { valid: false, reason: 'Buffer too small' };
|
|
143
154
|
}
|
|
@@ -185,8 +196,13 @@ function validateImageBuffer(buffer, url) {
|
|
|
185
196
|
return { valid: true, format: 'unknown' };
|
|
186
197
|
}
|
|
187
198
|
|
|
188
|
-
|
|
189
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Get file extension from format or URL
|
|
201
|
+
* @param {string} format - Image format detected
|
|
202
|
+
* @param {string} url - Original URL
|
|
203
|
+
* @returns {string} File extension with leading dot
|
|
204
|
+
*/
|
|
205
|
+
export function getExtensionFromFormat(format, url) {
|
|
190
206
|
const formatExtensions = {
|
|
191
207
|
png: '.png',
|
|
192
208
|
jpg: '.jpg',
|
|
@@ -227,8 +243,14 @@ function getExtensionFromFormat(format, url) {
|
|
|
227
243
|
return '.png'; // Default fallback
|
|
228
244
|
}
|
|
229
245
|
|
|
230
|
-
|
|
231
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Download a file with redirect support
|
|
248
|
+
* @param {string} url - URL to download
|
|
249
|
+
* @param {string} token - GitHub token for authenticated requests
|
|
250
|
+
* @param {number} maxRedirects - Maximum number of redirects to follow
|
|
251
|
+
* @returns {Promise<Buffer>} Downloaded file content
|
|
252
|
+
*/
|
|
253
|
+
export function downloadFile(url, token, maxRedirects = 5) {
|
|
232
254
|
return new Promise((resolve, reject) => {
|
|
233
255
|
if (maxRedirects <= 0) {
|
|
234
256
|
reject(new Error('Too many redirects'));
|
|
@@ -281,8 +303,12 @@ function downloadFile(url, token, maxRedirects = 5) {
|
|
|
281
303
|
});
|
|
282
304
|
}
|
|
283
305
|
|
|
284
|
-
|
|
285
|
-
|
|
306
|
+
/**
|
|
307
|
+
* Extract image URLs from markdown content
|
|
308
|
+
* @param {string} content - Markdown content to search
|
|
309
|
+
* @returns {Array<{url: string, alt: string}>} Array of image URLs with alt text
|
|
310
|
+
*/
|
|
311
|
+
export function extractMarkdownImageUrls(content) {
|
|
286
312
|
if (!content) {
|
|
287
313
|
return [];
|
|
288
314
|
}
|
|
@@ -305,8 +331,15 @@ function extractMarkdownImageUrls(content) {
|
|
|
305
331
|
return urls;
|
|
306
332
|
}
|
|
307
333
|
|
|
308
|
-
|
|
309
|
-
|
|
334
|
+
/**
|
|
335
|
+
* Download all images from content and update the markdown
|
|
336
|
+
* @param {string} content - Markdown content with image URLs
|
|
337
|
+
* @param {string} imagesDir - Directory to save images
|
|
338
|
+
* @param {string} token - GitHub token for authenticated requests
|
|
339
|
+
* @param {number} _prNumber - PR number (unused, kept for compatibility)
|
|
340
|
+
* @returns {Promise<{content: string, downloadedImages: Array}>} Updated content and downloaded images info
|
|
341
|
+
*/
|
|
342
|
+
export async function downloadImages(content, imagesDir, token, _prNumber) {
|
|
310
343
|
if (!content) {
|
|
311
344
|
return { content, downloadedImages: [] };
|
|
312
345
|
}
|
|
@@ -338,7 +371,7 @@ async function downloadImages(content, imagesDir, token, _prNumber) {
|
|
|
338
371
|
const ext = getExtensionFromFormat(validation.format, url);
|
|
339
372
|
const filename = `image-${imageCounter}${ext}`;
|
|
340
373
|
const localPath = path.join(imagesDir, filename);
|
|
341
|
-
const relativePath =
|
|
374
|
+
const relativePath = `./images/${filename}`;
|
|
342
375
|
|
|
343
376
|
await fs.writeFile(localPath, buffer);
|
|
344
377
|
downloadedImages.push({
|
|
@@ -362,87 +395,9 @@ async function downloadImages(content, imagesDir, token, _prNumber) {
|
|
|
362
395
|
return { content: updatedContent, downloadedImages };
|
|
363
396
|
}
|
|
364
397
|
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
log('blue', `🔍 Fetching pull request ${owner}/${repo}#${prNumber}...`);
|
|
369
|
-
|
|
370
|
-
const octokit = new Octokit({
|
|
371
|
-
auth: token,
|
|
372
|
-
baseUrl: 'https://api.github.com',
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
// Fetch PR data
|
|
376
|
-
const { data: pr } = await octokit.rest.pulls.get({
|
|
377
|
-
owner,
|
|
378
|
-
repo,
|
|
379
|
-
pull_number: prNumber,
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
// Fetch PR files
|
|
383
|
-
const { data: files } = await octokit.rest.pulls.listFiles({
|
|
384
|
-
owner,
|
|
385
|
-
repo,
|
|
386
|
-
pull_number: prNumber,
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
// Fetch PR comments
|
|
390
|
-
const { data: comments } = await octokit.rest.issues.listComments({
|
|
391
|
-
owner,
|
|
392
|
-
repo,
|
|
393
|
-
issue_number: prNumber,
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
// Fetch PR review comments
|
|
397
|
-
const { data: reviewComments } =
|
|
398
|
-
await octokit.rest.pulls.listReviewComments({
|
|
399
|
-
owner,
|
|
400
|
-
repo,
|
|
401
|
-
pull_number: prNumber,
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
// Fetch PR reviews (only if includeReviews is true)
|
|
405
|
-
let reviews = [];
|
|
406
|
-
if (options.includeReviews !== false) {
|
|
407
|
-
const { data: reviewsData } = await octokit.rest.pulls.listReviews({
|
|
408
|
-
owner,
|
|
409
|
-
repo,
|
|
410
|
-
pull_number: prNumber,
|
|
411
|
-
});
|
|
412
|
-
reviews = reviewsData;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Fetch PR commits
|
|
416
|
-
const { data: commits } = await octokit.rest.pulls.listCommits({
|
|
417
|
-
owner,
|
|
418
|
-
repo,
|
|
419
|
-
pull_number: prNumber,
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
log('green', `✅ Successfully fetched PR data`);
|
|
423
|
-
|
|
424
|
-
return {
|
|
425
|
-
pr,
|
|
426
|
-
files,
|
|
427
|
-
comments,
|
|
428
|
-
reviewComments,
|
|
429
|
-
reviews,
|
|
430
|
-
commits,
|
|
431
|
-
};
|
|
432
|
-
} catch (error) {
|
|
433
|
-
if (error.status === 404) {
|
|
434
|
-
log('red', `❌ Pull request not found: ${owner}/${repo}#${prNumber}`);
|
|
435
|
-
} else if (error.status === 401) {
|
|
436
|
-
log(
|
|
437
|
-
'red',
|
|
438
|
-
`❌ Authentication failed. Please provide a valid GitHub token`
|
|
439
|
-
);
|
|
440
|
-
} else {
|
|
441
|
-
log('red', `❌ Failed to fetch pull request: ${error.message}`);
|
|
442
|
-
}
|
|
443
|
-
process.exit(1);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
398
|
+
// Alias for backwards compatibility
|
|
399
|
+
export const fetchPullRequest = (owner, repo, prNumber, token, options = {}) =>
|
|
400
|
+
loadPullRequest({ owner, repo, prNumber, token, ...options });
|
|
446
401
|
|
|
447
402
|
// Process content and download images if enabled
|
|
448
403
|
function processContent(
|
|
@@ -458,8 +413,17 @@ function processContent(
|
|
|
458
413
|
return downloadImages(content, imagesDir, token, prNumber);
|
|
459
414
|
}
|
|
460
415
|
|
|
461
|
-
|
|
462
|
-
|
|
416
|
+
/**
|
|
417
|
+
* Convert PR data to markdown format
|
|
418
|
+
* @param {Object} data - PR data from loadPullRequest
|
|
419
|
+
* @param {Object} options - Conversion options
|
|
420
|
+
* @param {boolean} options.downloadImagesFlag - Download embedded images (default: true)
|
|
421
|
+
* @param {string} options.imagesDir - Directory to save images
|
|
422
|
+
* @param {string} options.token - GitHub token for downloading images
|
|
423
|
+
* @param {number} options.prNumber - PR number
|
|
424
|
+
* @returns {Promise<{markdown: string, downloadedImages: Array}>} Markdown content and downloaded images
|
|
425
|
+
*/
|
|
426
|
+
export async function convertToMarkdown(data, options = {}) {
|
|
463
427
|
const { pr, files, comments, reviewComments, reviews, commits } = data;
|
|
464
428
|
const {
|
|
465
429
|
downloadImagesFlag = true,
|
|
@@ -486,125 +450,152 @@ async function convertToMarkdown(data, options = {}) {
|
|
|
486
450
|
allDownloadedImages = [...allDownloadedImages, ...result.downloadedImages];
|
|
487
451
|
}
|
|
488
452
|
|
|
489
|
-
// Header
|
|
490
453
|
markdown += `# ${pr.title}\n\n`;
|
|
491
454
|
|
|
492
|
-
|
|
493
|
-
markdown +=
|
|
494
|
-
markdown += `**Created:** ${pr.created_at}\n`;
|
|
495
|
-
markdown += `**State:** ${pr.state}\n`;
|
|
496
|
-
markdown += `**Branch:** ${pr.head.ref} → ${pr.base.ref}\n`;
|
|
497
|
-
|
|
498
|
-
// Labels
|
|
499
|
-
if (pr.labels && pr.labels.length > 0) {
|
|
500
|
-
markdown += `**Labels:** ${pr.labels.map((l) => l.name).join(', ')}\n`;
|
|
501
|
-
}
|
|
455
|
+
markdown += generateMetadataMarkdown(pr);
|
|
456
|
+
markdown += '---\n\n';
|
|
502
457
|
|
|
503
|
-
markdown += '
|
|
458
|
+
markdown += '## Description\n\n';
|
|
504
459
|
markdown += prBody ? `${prBody}\n\n` : '_No description provided._\n\n';
|
|
505
460
|
|
|
506
461
|
markdown += '---\n\n';
|
|
507
462
|
|
|
508
|
-
|
|
509
|
-
if (comments.length > 0) {
|
|
510
|
-
markdown += `## Comments\n\n`;
|
|
511
|
-
for (const comment of comments) {
|
|
512
|
-
let commentBody = comment.body || '';
|
|
513
|
-
if (downloadImagesFlag && commentBody) {
|
|
514
|
-
verboseLog(
|
|
515
|
-
'blue',
|
|
516
|
-
`Processing images in comment by @${comment.user.login}...`
|
|
517
|
-
);
|
|
518
|
-
const result = await processContent(
|
|
519
|
-
commentBody,
|
|
520
|
-
imagesDir,
|
|
521
|
-
token,
|
|
522
|
-
prNumber,
|
|
523
|
-
downloadImagesFlag
|
|
524
|
-
);
|
|
525
|
-
commentBody = result.content;
|
|
526
|
-
allDownloadedImages = [
|
|
527
|
-
...allDownloadedImages,
|
|
528
|
-
...result.downloadedImages,
|
|
529
|
-
];
|
|
530
|
-
}
|
|
463
|
+
markdown += '## Conversation\n\n';
|
|
531
464
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
465
|
+
const timelineEvents = [];
|
|
466
|
+
|
|
467
|
+
for (const comment of comments) {
|
|
468
|
+
timelineEvents.push({
|
|
469
|
+
type: 'comment',
|
|
470
|
+
timestamp: new Date(comment.created_at),
|
|
471
|
+
data: comment,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
for (const review of reviews) {
|
|
476
|
+
if (review.submitted_at) {
|
|
477
|
+
timelineEvents.push({
|
|
478
|
+
type: 'review',
|
|
479
|
+
timestamp: new Date(review.submitted_at),
|
|
480
|
+
data: review,
|
|
481
|
+
});
|
|
535
482
|
}
|
|
536
483
|
}
|
|
537
484
|
|
|
538
|
-
|
|
539
|
-
if (reviews.length > 0) {
|
|
540
|
-
markdown += `## Reviews\n\n`;
|
|
541
|
-
for (const review of reviews) {
|
|
542
|
-
let reviewBody = review.body || '';
|
|
543
|
-
if (downloadImagesFlag && reviewBody) {
|
|
544
|
-
verboseLog(
|
|
545
|
-
'blue',
|
|
546
|
-
`Processing images in review by @${review.user.login}...`
|
|
547
|
-
);
|
|
548
|
-
const result = await processContent(
|
|
549
|
-
reviewBody,
|
|
550
|
-
imagesDir,
|
|
551
|
-
token,
|
|
552
|
-
prNumber,
|
|
553
|
-
downloadImagesFlag
|
|
554
|
-
);
|
|
555
|
-
reviewBody = result.content;
|
|
556
|
-
allDownloadedImages = [
|
|
557
|
-
...allDownloadedImages,
|
|
558
|
-
...result.downloadedImages,
|
|
559
|
-
];
|
|
560
|
-
}
|
|
485
|
+
timelineEvents.sort((a, b) => a.timestamp - b.timestamp);
|
|
561
486
|
|
|
562
|
-
|
|
563
|
-
|
|
487
|
+
if (timelineEvents.length === 0) {
|
|
488
|
+
markdown += '_No comments or reviews._\n\n';
|
|
489
|
+
} else {
|
|
490
|
+
for (const event of timelineEvents) {
|
|
491
|
+
if (event.type === 'comment') {
|
|
492
|
+
const comment = event.data;
|
|
493
|
+
let commentBody = comment.body || '';
|
|
494
|
+
if (downloadImagesFlag && commentBody) {
|
|
495
|
+
verboseLog(
|
|
496
|
+
'blue',
|
|
497
|
+
`Processing images in comment by @${comment.user.login}...`
|
|
498
|
+
);
|
|
499
|
+
const result = await processContent(
|
|
500
|
+
commentBody,
|
|
501
|
+
imagesDir,
|
|
502
|
+
token,
|
|
503
|
+
prNumber,
|
|
504
|
+
downloadImagesFlag
|
|
505
|
+
);
|
|
506
|
+
commentBody = result.content;
|
|
507
|
+
allDownloadedImages = [
|
|
508
|
+
...allDownloadedImages,
|
|
509
|
+
...result.downloadedImages,
|
|
510
|
+
];
|
|
511
|
+
}
|
|
564
512
|
|
|
565
|
-
|
|
566
|
-
markdown +=
|
|
567
|
-
|
|
513
|
+
markdown += `### 💬 Comment by [@${comment.user.login}](https://github.com/${comment.user.login})\n`;
|
|
514
|
+
markdown += `*${formatDate(comment.created_at)}*\n\n`;
|
|
515
|
+
markdown += `${commentBody}\n\n`;
|
|
516
|
+
markdown += '---\n\n';
|
|
517
|
+
} else if (event.type === 'review') {
|
|
518
|
+
const review = event.data;
|
|
519
|
+
let reviewBody = review.body || '';
|
|
520
|
+
if (downloadImagesFlag && reviewBody) {
|
|
521
|
+
verboseLog(
|
|
522
|
+
'blue',
|
|
523
|
+
`Processing images in review by @${review.user.login}...`
|
|
524
|
+
);
|
|
525
|
+
const result = await processContent(
|
|
526
|
+
reviewBody,
|
|
527
|
+
imagesDir,
|
|
528
|
+
token,
|
|
529
|
+
prNumber,
|
|
530
|
+
downloadImagesFlag
|
|
531
|
+
);
|
|
532
|
+
reviewBody = result.content;
|
|
533
|
+
allDownloadedImages = [
|
|
534
|
+
...allDownloadedImages,
|
|
535
|
+
...result.downloadedImages,
|
|
536
|
+
];
|
|
537
|
+
}
|
|
568
538
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
imagesDir,
|
|
581
|
-
token,
|
|
582
|
-
prNumber,
|
|
583
|
-
downloadImagesFlag
|
|
584
|
-
);
|
|
585
|
-
rcBody = result.content;
|
|
586
|
-
allDownloadedImages = [
|
|
587
|
-
...allDownloadedImages,
|
|
588
|
-
...result.downloadedImages,
|
|
589
|
-
];
|
|
590
|
-
}
|
|
539
|
+
const stateEmoji =
|
|
540
|
+
review.state === 'APPROVED'
|
|
541
|
+
? '✅'
|
|
542
|
+
: review.state === 'CHANGES_REQUESTED'
|
|
543
|
+
? '❌'
|
|
544
|
+
: review.state === 'COMMENTED'
|
|
545
|
+
? '💬'
|
|
546
|
+
: '📝';
|
|
547
|
+
|
|
548
|
+
markdown += `### ${stateEmoji} Review by [@${review.user.login}](https://github.com/${review.user.login})\n`;
|
|
549
|
+
markdown += `*${formatDate(review.submitted_at)}* — **${review.state}**\n\n`;
|
|
591
550
|
|
|
592
|
-
|
|
593
|
-
markdown +=
|
|
594
|
-
markdown += `${rcBody}\n\n`;
|
|
551
|
+
if (reviewBody) {
|
|
552
|
+
markdown += `${reviewBody}\n\n`;
|
|
595
553
|
}
|
|
596
|
-
}
|
|
597
554
|
|
|
598
|
-
|
|
555
|
+
// Add review comments for this review
|
|
556
|
+
const reviewReviewComments = reviewComments.filter(
|
|
557
|
+
(rc) => rc.pull_request_review_id === review.id
|
|
558
|
+
);
|
|
559
|
+
if (reviewReviewComments.length > 0) {
|
|
560
|
+
markdown += `#### Inline Comments\n\n`;
|
|
561
|
+
for (const rc of reviewReviewComments) {
|
|
562
|
+
let rcBody = rc.body || '';
|
|
563
|
+
if (downloadImagesFlag && rcBody) {
|
|
564
|
+
const result = await processContent(
|
|
565
|
+
rcBody,
|
|
566
|
+
imagesDir,
|
|
567
|
+
token,
|
|
568
|
+
prNumber,
|
|
569
|
+
downloadImagesFlag
|
|
570
|
+
);
|
|
571
|
+
rcBody = result.content;
|
|
572
|
+
allDownloadedImages = [
|
|
573
|
+
...allDownloadedImages,
|
|
574
|
+
...result.downloadedImages,
|
|
575
|
+
];
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const lineInfo = rc.line ? `:${rc.line}` : '';
|
|
579
|
+
markdown += `**\`${rc.path}${lineInfo}\`**\n\n`;
|
|
580
|
+
markdown += `${rcBody}\n\n`;
|
|
581
|
+
if (rc.diff_hunk) {
|
|
582
|
+
markdown += '```diff\n';
|
|
583
|
+
markdown += `${rc.diff_hunk}\n`;
|
|
584
|
+
markdown += '```\n\n';
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
markdown += '---\n\n';
|
|
590
|
+
}
|
|
599
591
|
}
|
|
600
592
|
}
|
|
601
593
|
|
|
602
|
-
// Standalone review comments (not associated with a review)
|
|
603
594
|
const standaloneReviewComments = reviewComments.filter(
|
|
604
595
|
(rc) => !rc.pull_request_review_id
|
|
605
596
|
);
|
|
606
597
|
if (standaloneReviewComments.length > 0) {
|
|
607
|
-
markdown += `##
|
|
598
|
+
markdown += `## Inline Code Comments\n\n`;
|
|
608
599
|
for (const comment of standaloneReviewComments) {
|
|
609
600
|
let commentBody = comment.body || '';
|
|
610
601
|
if (downloadImagesFlag && commentBody) {
|
|
@@ -622,191 +613,208 @@ async function convertToMarkdown(data, options = {}) {
|
|
|
622
613
|
];
|
|
623
614
|
}
|
|
624
615
|
|
|
625
|
-
markdown +=
|
|
616
|
+
markdown += `### [@${comment.user.login}](https://github.com/${comment.user.login}) on \`${comment.path}\``;
|
|
626
617
|
if (comment.line) {
|
|
627
618
|
markdown += ` (line ${comment.line})`;
|
|
628
619
|
}
|
|
629
|
-
markdown +=
|
|
630
|
-
markdown += `*${comment.created_at}*\n\n`;
|
|
620
|
+
markdown += `\n`;
|
|
621
|
+
markdown += `*${formatDate(comment.created_at)}*\n\n`;
|
|
631
622
|
markdown += `${commentBody}\n\n`;
|
|
632
623
|
if (comment.diff_hunk) {
|
|
633
624
|
markdown += '```diff\n';
|
|
634
625
|
markdown += `${comment.diff_hunk}\n`;
|
|
635
626
|
markdown += '```\n\n';
|
|
636
627
|
}
|
|
628
|
+
markdown += '---\n\n';
|
|
637
629
|
}
|
|
638
630
|
}
|
|
639
631
|
|
|
640
|
-
|
|
641
|
-
if (commits.length > 0) {
|
|
642
|
-
markdown += `## Commits (${commits.length})\n\n`;
|
|
643
|
-
for (const commit of commits) {
|
|
644
|
-
const message = commit.commit.message.split('\n')[0]; // First line only
|
|
645
|
-
const sha = commit.sha.substring(0, 7);
|
|
646
|
-
markdown += `- [\`${sha}\`](${commit.html_url}) ${message} - @${commit.author?.login || 'unknown'}\n`;
|
|
647
|
-
}
|
|
648
|
-
markdown += '\n';
|
|
649
|
-
}
|
|
632
|
+
markdown += generateCommitsMarkdown(commits);
|
|
650
633
|
|
|
651
|
-
|
|
652
|
-
if (files.length > 0) {
|
|
653
|
-
markdown += `## Files Changed (${files.length})\n\n`;
|
|
654
|
-
for (const file of files) {
|
|
655
|
-
const statusIcon =
|
|
656
|
-
file.status === 'added'
|
|
657
|
-
? '🆕'
|
|
658
|
-
: file.status === 'removed'
|
|
659
|
-
? '🗑️'
|
|
660
|
-
: file.status === 'modified'
|
|
661
|
-
? '✏️'
|
|
662
|
-
: file.status === 'renamed'
|
|
663
|
-
? '📝'
|
|
664
|
-
: '📄';
|
|
665
|
-
markdown += `${statusIcon} **${file.filename}** (+${file.additions} -${file.deletions})\n`;
|
|
666
|
-
if (file.status === 'renamed') {
|
|
667
|
-
markdown += ` - Renamed from: \`${file.previous_filename}\`\n`;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
markdown += '\n';
|
|
671
|
-
}
|
|
634
|
+
markdown += generateFilesMarkdown(files);
|
|
672
635
|
|
|
673
636
|
return { markdown, downloadedImages: allDownloadedImages };
|
|
674
637
|
}
|
|
675
638
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
639
|
+
/**
|
|
640
|
+
* Convert PR data to JSON format
|
|
641
|
+
* @param {Object} data - PR data from loadPullRequest
|
|
642
|
+
* @param {Array} downloadedImages - Array of downloaded image info
|
|
643
|
+
* @returns {string} JSON string
|
|
644
|
+
*/
|
|
645
|
+
export function convertToJson(data, downloadedImages = []) {
|
|
646
|
+
return formattersConvertToJson(data, downloadedImages);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Save PR data to a folder with all assets for offline viewing
|
|
651
|
+
* @param {Object} data - PR data from loadPullRequest
|
|
652
|
+
* @param {Object} options - Save options
|
|
653
|
+
* @param {string} options.outputDir - Output directory
|
|
654
|
+
* @param {string} options.format - Output format ('markdown' or 'json')
|
|
655
|
+
* @param {boolean} options.downloadImages - Download images (default: true)
|
|
656
|
+
* @param {string} options.token - GitHub token for downloading images
|
|
657
|
+
* @returns {Promise<{mdPath: string, jsonPath: string, imagesDir: string, downloadedImages: Array}>}
|
|
658
|
+
*/
|
|
659
|
+
export async function savePullRequest(data, options = {}) {
|
|
660
|
+
const {
|
|
661
|
+
outputDir,
|
|
662
|
+
format = 'markdown',
|
|
663
|
+
downloadImages: downloadImagesFlag = true,
|
|
664
|
+
token = '',
|
|
665
|
+
} = options;
|
|
666
|
+
|
|
667
|
+
const prNumber = data.pr.number;
|
|
668
|
+
const prDir = path.join(outputDir, `pr-${prNumber}`);
|
|
669
|
+
const imagesDir = path.join(prDir, 'images');
|
|
670
|
+
const mdPath = path.join(prDir, `pr-${prNumber}.md`);
|
|
671
|
+
const jsonPath = path.join(prDir, `pr-${prNumber}.json`);
|
|
672
|
+
|
|
673
|
+
// Ensure directories exist
|
|
674
|
+
await fs.ensureDir(prDir);
|
|
675
|
+
|
|
676
|
+
let downloadedImages = [];
|
|
677
|
+
|
|
678
|
+
// Generate markdown
|
|
679
|
+
log('blue', `📝 Converting to ${format}...`);
|
|
680
|
+
|
|
681
|
+
const mdResult = await convertToMarkdown(data, {
|
|
682
|
+
downloadImagesFlag,
|
|
683
|
+
imagesDir,
|
|
684
|
+
token,
|
|
685
|
+
prNumber,
|
|
686
|
+
});
|
|
687
|
+
downloadedImages = mdResult.downloadedImages;
|
|
679
688
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
line: c.line,
|
|
726
|
-
createdAt: c.created_at,
|
|
727
|
-
diffHunk: c.diff_hunk,
|
|
728
|
-
reviewId: c.pull_request_review_id,
|
|
729
|
-
})),
|
|
730
|
-
comments: comments.map((c) => ({
|
|
731
|
-
id: c.id,
|
|
732
|
-
author: c.user.login,
|
|
733
|
-
body: c.body,
|
|
734
|
-
createdAt: c.created_at,
|
|
735
|
-
})),
|
|
736
|
-
downloadedImages: downloadedImages.map((img) => ({
|
|
737
|
-
originalUrl: img.originalUrl,
|
|
738
|
-
localPath: img.relativePath,
|
|
739
|
-
format: img.format,
|
|
740
|
-
})),
|
|
741
|
-
},
|
|
742
|
-
null,
|
|
743
|
-
2
|
|
689
|
+
// Save markdown
|
|
690
|
+
await fs.writeFile(mdPath, mdResult.markdown, 'utf8');
|
|
691
|
+
log('green', `✅ Saved markdown to ${mdPath}`);
|
|
692
|
+
|
|
693
|
+
// Always save JSON as well for metadata
|
|
694
|
+
const jsonContent = convertToJson(data, downloadedImages);
|
|
695
|
+
await fs.writeFile(jsonPath, jsonContent, 'utf8');
|
|
696
|
+
log('green', `✅ Saved JSON metadata to ${jsonPath}`);
|
|
697
|
+
|
|
698
|
+
if (downloadedImages.length > 0) {
|
|
699
|
+
log(
|
|
700
|
+
'green',
|
|
701
|
+
`📁 Downloaded ${downloadedImages.length} image(s) to ${imagesDir}`
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return {
|
|
706
|
+
mdPath,
|
|
707
|
+
jsonPath,
|
|
708
|
+
imagesDir,
|
|
709
|
+
downloadedImages,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// CLI IMPLEMENTATION
|
|
714
|
+
|
|
715
|
+
// Only run CLI when executed directly, not when imported as a module
|
|
716
|
+
// Check if this module is the main entry point by comparing paths
|
|
717
|
+
function isRunningAsCli() {
|
|
718
|
+
// Get the actual script being run
|
|
719
|
+
const scriptArg = process.argv[1];
|
|
720
|
+
if (!scriptArg) {
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Normalize paths for comparison
|
|
725
|
+
const scriptPath = path.resolve(scriptArg);
|
|
726
|
+
const thisModulePath = path.resolve(__filename);
|
|
727
|
+
|
|
728
|
+
// Check if the script being run is this module
|
|
729
|
+
// This handles both direct execution and bun run
|
|
730
|
+
return (
|
|
731
|
+
scriptPath === thisModulePath ||
|
|
732
|
+
scriptPath.endsWith('gh-load-pull-request.mjs') ||
|
|
733
|
+
scriptPath.endsWith('gh-load-pull-request')
|
|
744
734
|
);
|
|
745
735
|
}
|
|
746
736
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
.
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
(
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
'
|
|
807
|
-
|
|
737
|
+
/**
|
|
738
|
+
* Parse CLI arguments
|
|
739
|
+
* @returns {Object} Parsed CLI arguments
|
|
740
|
+
*/
|
|
741
|
+
function parseCliArgs() {
|
|
742
|
+
const scriptName = path.basename(process.argv[1] || 'gh-load-pull-request');
|
|
743
|
+
return yargs(hideBin(process.argv))
|
|
744
|
+
.scriptName(scriptName)
|
|
745
|
+
.version(version)
|
|
746
|
+
.usage('Usage: $0 <pr-url> [options]')
|
|
747
|
+
.command(
|
|
748
|
+
'$0 <pr>',
|
|
749
|
+
'Download a GitHub pull request and convert it to markdown',
|
|
750
|
+
(yargs) => {
|
|
751
|
+
yargs.positional('pr', {
|
|
752
|
+
describe:
|
|
753
|
+
'Pull request URL or shorthand (e.g., https://github.com/owner/repo/pull/123 or owner/repo#123)',
|
|
754
|
+
type: 'string',
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
)
|
|
758
|
+
.option('token', {
|
|
759
|
+
alias: 't',
|
|
760
|
+
type: 'string',
|
|
761
|
+
describe: 'GitHub personal access token (optional for public PRs)',
|
|
762
|
+
default: process.env.GITHUB_TOKEN,
|
|
763
|
+
})
|
|
764
|
+
.option('output', {
|
|
765
|
+
alias: 'o',
|
|
766
|
+
type: 'string',
|
|
767
|
+
describe: 'Output directory (creates pr-<number>/ subfolder)',
|
|
768
|
+
})
|
|
769
|
+
.option('download-images', {
|
|
770
|
+
type: 'boolean',
|
|
771
|
+
describe: 'Download embedded images',
|
|
772
|
+
default: true,
|
|
773
|
+
})
|
|
774
|
+
.option('include-reviews', {
|
|
775
|
+
type: 'boolean',
|
|
776
|
+
describe: 'Include PR reviews',
|
|
777
|
+
default: true,
|
|
778
|
+
})
|
|
779
|
+
.option('format', {
|
|
780
|
+
type: 'string',
|
|
781
|
+
describe: 'Output format: markdown, json',
|
|
782
|
+
default: 'markdown',
|
|
783
|
+
choices: ['markdown', 'json'],
|
|
784
|
+
})
|
|
785
|
+
.option('verbose', {
|
|
786
|
+
alias: 'v',
|
|
787
|
+
type: 'boolean',
|
|
788
|
+
describe: 'Enable verbose logging',
|
|
789
|
+
default: false,
|
|
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
|
+
})
|
|
801
|
+
.help('h')
|
|
802
|
+
.alias('h', 'help')
|
|
803
|
+
.example('$0 https://github.com/owner/repo/pull/123', 'Download PR #123')
|
|
804
|
+
.example('$0 owner/repo#123', 'Download PR using shorthand format')
|
|
805
|
+
.example('$0 owner/repo#123 -o ./output', 'Save to output directory')
|
|
806
|
+
.example('$0 owner/repo#123 --format json', 'Output as JSON')
|
|
807
|
+
.example('$0 owner/repo#123 --no-download-images', 'Skip image download')
|
|
808
|
+
.example(
|
|
809
|
+
'$0 https://github.com/owner/repo/pull/123 --token ghp_xxx',
|
|
810
|
+
'Download private PR'
|
|
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;
|
|
814
|
+
}
|
|
808
815
|
|
|
809
816
|
async function main() {
|
|
817
|
+
const argv = parseCliArgs();
|
|
810
818
|
const {
|
|
811
819
|
pr: prInput,
|
|
812
820
|
token: tokenArg,
|
|
@@ -815,11 +823,22 @@ async function main() {
|
|
|
815
823
|
'include-reviews': includeReviews,
|
|
816
824
|
format,
|
|
817
825
|
verbose,
|
|
826
|
+
'force-api': forceApi,
|
|
827
|
+
'force-gh': forceGh,
|
|
818
828
|
} = argv;
|
|
819
829
|
|
|
820
830
|
// Set verbose mode
|
|
821
831
|
verboseMode = verbose;
|
|
822
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
|
+
|
|
823
842
|
// Parse PR input first (before potentially slow gh CLI token fetch)
|
|
824
843
|
const prInfo = parsePrUrl(prInput);
|
|
825
844
|
if (!prInfo) {
|
|
@@ -833,91 +852,75 @@ async function main() {
|
|
|
833
852
|
|
|
834
853
|
let token = tokenArg;
|
|
835
854
|
|
|
836
|
-
// If no token provided, try to get
|
|
837
|
-
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) {
|
|
838
857
|
const ghToken = await getGhToken();
|
|
839
858
|
if (ghToken) {
|
|
840
859
|
token = ghToken;
|
|
841
|
-
|
|
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
|
+
}
|
|
842
865
|
}
|
|
843
866
|
}
|
|
844
867
|
|
|
845
868
|
const { owner, repo, prNumber } = prInfo;
|
|
846
869
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
// Convert to appropriate format
|
|
859
|
-
log('blue', `📝 Converting to ${format}...`);
|
|
860
|
-
|
|
861
|
-
let outputContent;
|
|
862
|
-
let downloadedImages = [];
|
|
870
|
+
try {
|
|
871
|
+
// Fetch PR data
|
|
872
|
+
const data = await loadPullRequest({
|
|
873
|
+
owner,
|
|
874
|
+
repo,
|
|
875
|
+
prNumber,
|
|
876
|
+
token,
|
|
877
|
+
includeReviews,
|
|
878
|
+
forceApi,
|
|
879
|
+
forceGh,
|
|
880
|
+
});
|
|
863
881
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
...data.comments.map((c) => c.body || ''),
|
|
872
|
-
...data.reviews.map((r) => r.body || ''),
|
|
873
|
-
...data.reviewComments.map((rc) => rc.body || ''),
|
|
874
|
-
].join('\n\n');
|
|
875
|
-
|
|
876
|
-
const result = await downloadImages(
|
|
877
|
-
allContent,
|
|
878
|
-
imagesDir,
|
|
882
|
+
// Determine output paths
|
|
883
|
+
if (output) {
|
|
884
|
+
// Save to directory
|
|
885
|
+
await savePullRequest(data, {
|
|
886
|
+
outputDir: output,
|
|
887
|
+
format,
|
|
888
|
+
downloadImages: downloadImagesFlag,
|
|
879
889
|
token,
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
890
|
+
});
|
|
891
|
+
} else {
|
|
892
|
+
// Output to stdout
|
|
893
|
+
if (format === 'json') {
|
|
894
|
+
const jsonContent = convertToJson(data, []);
|
|
895
|
+
console.log(jsonContent);
|
|
896
|
+
} else {
|
|
897
|
+
const { markdown } = await convertToMarkdown(data, {
|
|
898
|
+
downloadImagesFlag: false, // Don't download images when outputting to stdout
|
|
899
|
+
imagesDir: '',
|
|
900
|
+
token: '',
|
|
901
|
+
prNumber,
|
|
902
|
+
});
|
|
903
|
+
console.log(markdown);
|
|
904
|
+
}
|
|
883
905
|
}
|
|
884
|
-
outputContent = convertToJson(data, downloadedImages);
|
|
885
|
-
} else {
|
|
886
|
-
// Markdown format with image processing
|
|
887
|
-
const result = await convertToMarkdown(data, {
|
|
888
|
-
downloadImagesFlag,
|
|
889
|
-
imagesDir,
|
|
890
|
-
token,
|
|
891
|
-
prNumber,
|
|
892
|
-
});
|
|
893
|
-
outputContent = result.markdown;
|
|
894
|
-
downloadedImages = result.downloadedImages;
|
|
895
|
-
}
|
|
896
906
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
log('green', `✅ Saved to ${outputPath}`);
|
|
903
|
-
|
|
904
|
-
if (downloadedImages.length > 0) {
|
|
905
|
-
log(
|
|
906
|
-
'green',
|
|
907
|
-
`📁 Downloaded ${downloadedImages.length} image(s) to ${imagesDir}`
|
|
908
|
-
);
|
|
907
|
+
log('blue', '🎉 Done!');
|
|
908
|
+
} catch (error) {
|
|
909
|
+
log('red', `❌ ${error.message}`);
|
|
910
|
+
if (verboseMode) {
|
|
911
|
+
console.error(error.stack);
|
|
909
912
|
}
|
|
910
|
-
|
|
911
|
-
console.log(outputContent);
|
|
913
|
+
process.exit(1);
|
|
912
914
|
}
|
|
913
|
-
|
|
914
|
-
log('blue', '🎉 Done!');
|
|
915
915
|
}
|
|
916
916
|
|
|
917
|
-
main
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
}
|
|
917
|
+
// Run CLI if this is the main module
|
|
918
|
+
if (isRunningAsCli()) {
|
|
919
|
+
main().catch((error) => {
|
|
920
|
+
log('red', `💥 Script failed: ${error.message}`);
|
|
921
|
+
if (verboseMode) {
|
|
922
|
+
console.error(error.stack);
|
|
923
|
+
}
|
|
924
|
+
process.exit(1);
|
|
925
|
+
});
|
|
926
|
+
}
|