git-ripper 1.4.5 → 1.4.6
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/package.json +1 -1
- package/src/archiver.js +24 -2
- package/src/downloader.js +136 -81
- package/src/index.js +35 -13
package/package.json
CHANGED
package/src/archiver.js
CHANGED
|
@@ -144,10 +144,32 @@ export const downloadAndArchive = async (
|
|
|
144
144
|
// Create a temporary directory for the download
|
|
145
145
|
const tempDir = path.join(outputDir, `.temp-${Date.now()}`);
|
|
146
146
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
147
|
-
|
|
148
147
|
try {
|
|
149
148
|
// Download the folder contents
|
|
150
|
-
await downloadFolder(repoInfo, tempDir);
|
|
149
|
+
const downloadResult = await downloadFolder(repoInfo, tempDir);
|
|
150
|
+
|
|
151
|
+
// Check if download failed
|
|
152
|
+
if (downloadResult && !downloadResult.success) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Download failed: ${
|
|
155
|
+
downloadResult.failedFiles || 0
|
|
156
|
+
} files could not be downloaded`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check if there's anything to archive
|
|
161
|
+
const files = fs
|
|
162
|
+
.readdirSync(tempDir, { recursive: true })
|
|
163
|
+
.filter((file) => {
|
|
164
|
+
const fullPath = path.join(tempDir, file);
|
|
165
|
+
return fs.statSync(fullPath).isFile();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (files.length === 0) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`No files to archive - download may have failed or repository is empty`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
151
173
|
|
|
152
174
|
// Determine archive filename
|
|
153
175
|
let archiveFileName = archiveName;
|
package/src/downloader.js
CHANGED
|
@@ -45,6 +45,7 @@ const getSpinnerFrame = () => {
|
|
|
45
45
|
* @param {string} branch - Branch name
|
|
46
46
|
* @param {string} folderPath - Path to the folder
|
|
47
47
|
* @returns {Promise<Array>} - Promise resolving to an array of file objects
|
|
48
|
+
* @throws {Error} - Throws error on API failures instead of returning empty array
|
|
48
49
|
*/
|
|
49
50
|
const fetchFolderContents = async (owner, repo, branch, folderPath) => {
|
|
50
51
|
let effectiveBranch = branch;
|
|
@@ -55,12 +56,9 @@ const fetchFolderContents = async (owner, repo, branch, folderPath) => {
|
|
|
55
56
|
const repoInfoResponse = await axios.get(repoInfoUrl);
|
|
56
57
|
effectiveBranch = repoInfoResponse.data.default_branch;
|
|
57
58
|
if (!effectiveBranch) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
`Could not determine default branch for ${owner}/${repo}. Please specify a branch in the URL.`
|
|
61
|
-
)
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Could not determine default branch for ${owner}/${repo}. Please specify a branch in the URL.`
|
|
62
61
|
);
|
|
63
|
-
return [];
|
|
64
62
|
}
|
|
65
63
|
console.log(
|
|
66
64
|
chalk.blue(
|
|
@@ -68,12 +66,12 @@ const fetchFolderContents = async (owner, repo, branch, folderPath) => {
|
|
|
68
66
|
)
|
|
69
67
|
);
|
|
70
68
|
} catch (error) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
if (error.message.includes("Could not determine default branch")) {
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Failed to fetch default branch for ${owner}/${repo}: ${error.message}`
|
|
75
74
|
);
|
|
76
|
-
return [];
|
|
77
75
|
}
|
|
78
76
|
}
|
|
79
77
|
|
|
@@ -109,54 +107,44 @@ const fetchFolderContents = async (owner, repo, branch, folderPath) => {
|
|
|
109
107
|
return response.data.tree.filter((item) => item.path.startsWith(prefix));
|
|
110
108
|
}
|
|
111
109
|
} catch (error) {
|
|
110
|
+
let errorMessage = "";
|
|
111
|
+
let isRateLimit = false;
|
|
112
|
+
|
|
112
113
|
if (error.response) {
|
|
113
114
|
// Handle specific HTTP error codes
|
|
114
115
|
switch (error.response.status) {
|
|
115
116
|
case 403:
|
|
116
117
|
if (error.response.headers["x-ratelimit-remaining"] === "0") {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
).toLocaleTimeString()} or add a GitHub token (feature coming soon).`
|
|
122
|
-
)
|
|
123
|
-
);
|
|
118
|
+
isRateLimit = true;
|
|
119
|
+
errorMessage = `GitHub API rate limit exceeded. Please wait until ${new Date(
|
|
120
|
+
parseInt(error.response.headers["x-ratelimit-reset"]) * 1000
|
|
121
|
+
).toLocaleTimeString()} or add a GitHub token (feature coming soon).`;
|
|
124
122
|
} else {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}`
|
|
130
|
-
)
|
|
131
|
-
);
|
|
123
|
+
errorMessage = `Access forbidden: ${
|
|
124
|
+
error.response.data.message ||
|
|
125
|
+
"Repository may be private or you may not have access"
|
|
126
|
+
}`;
|
|
132
127
|
}
|
|
133
128
|
break;
|
|
134
129
|
case 404:
|
|
135
|
-
|
|
136
|
-
chalk.red(
|
|
137
|
-
`Repository, branch, or folder not found: ${owner}/${repo}/${branch}/${folderPath}`
|
|
138
|
-
)
|
|
139
|
-
);
|
|
130
|
+
errorMessage = `Repository, branch, or folder not found: ${owner}/${repo}/${branch}/${folderPath}`;
|
|
140
131
|
break;
|
|
141
132
|
default:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
error.response.data.message || error.message
|
|
146
|
-
}`
|
|
147
|
-
)
|
|
148
|
-
);
|
|
133
|
+
errorMessage = `API error (${error.response.status}): ${
|
|
134
|
+
error.response.data.message || error.message
|
|
135
|
+
}`;
|
|
149
136
|
}
|
|
150
137
|
} else if (error.request) {
|
|
151
|
-
|
|
152
|
-
chalk.red(
|
|
153
|
-
`Network error: No response received from GitHub. Please check your internet connection.`
|
|
154
|
-
)
|
|
155
|
-
);
|
|
138
|
+
errorMessage = `Network error: No response received from GitHub. Please check your internet connection.`;
|
|
156
139
|
} else {
|
|
157
|
-
|
|
140
|
+
errorMessage = `Error preparing request: ${error.message}`;
|
|
158
141
|
}
|
|
159
|
-
|
|
142
|
+
|
|
143
|
+
// Always throw the error instead of returning empty array
|
|
144
|
+
const enrichedError = new Error(errorMessage);
|
|
145
|
+
enrichedError.isRateLimit = isRateLimit;
|
|
146
|
+
enrichedError.statusCode = error.response?.status;
|
|
147
|
+
throw enrichedError;
|
|
160
148
|
}
|
|
161
149
|
};
|
|
162
150
|
|
|
@@ -349,11 +337,15 @@ const downloadFolder = async (
|
|
|
349
337
|
const contents = await fetchFolderContents(owner, repo, branch, folderPath);
|
|
350
338
|
|
|
351
339
|
if (!contents || contents.length === 0) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
340
|
+
const message = `No files found in ${folderPath || "repository root"}`;
|
|
341
|
+
console.log(chalk.yellow(message));
|
|
342
|
+
// Don't print success message when no files are found - this might indicate an error
|
|
343
|
+
return {
|
|
344
|
+
success: true,
|
|
345
|
+
filesDownloaded: 0,
|
|
346
|
+
failedFiles: 0,
|
|
347
|
+
isEmpty: true,
|
|
348
|
+
};
|
|
357
349
|
}
|
|
358
350
|
|
|
359
351
|
// Filter for blob type (files)
|
|
@@ -361,15 +353,18 @@ const downloadFolder = async (
|
|
|
361
353
|
const totalFiles = files.length;
|
|
362
354
|
|
|
363
355
|
if (totalFiles === 0) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
356
|
+
const message = `No files found in ${
|
|
357
|
+
folderPath || "repository root"
|
|
358
|
+
} (only directories)`;
|
|
359
|
+
console.log(chalk.yellow(message));
|
|
360
|
+
// This is a legitimate case - directory exists but contains only subdirectories
|
|
361
|
+
console.log(chalk.green(`Directory structure downloaded successfully!`));
|
|
362
|
+
return {
|
|
363
|
+
success: true,
|
|
364
|
+
filesDownloaded: 0,
|
|
365
|
+
failedFiles: 0,
|
|
366
|
+
isEmpty: true,
|
|
367
|
+
};
|
|
373
368
|
}
|
|
374
369
|
|
|
375
370
|
console.log(
|
|
@@ -464,7 +459,6 @@ const downloadFolder = async (
|
|
|
464
459
|
// Count successful and failed downloads
|
|
465
460
|
const succeeded = results.filter((r) => r.success).length;
|
|
466
461
|
const failed = failedFiles.length;
|
|
467
|
-
|
|
468
462
|
if (failed > 0) {
|
|
469
463
|
console.log(
|
|
470
464
|
chalk.yellow(
|
|
@@ -485,15 +479,45 @@ const downloadFolder = async (
|
|
|
485
479
|
)
|
|
486
480
|
);
|
|
487
481
|
}
|
|
482
|
+
|
|
483
|
+
// Don't claim success if files failed to download
|
|
484
|
+
if (succeeded === 0) {
|
|
485
|
+
console.log(
|
|
486
|
+
chalk.red(`❌ Download failed: No files were downloaded successfully`)
|
|
487
|
+
);
|
|
488
|
+
return {
|
|
489
|
+
success: false,
|
|
490
|
+
filesDownloaded: succeeded,
|
|
491
|
+
failedFiles: failed,
|
|
492
|
+
isEmpty: false,
|
|
493
|
+
};
|
|
494
|
+
} else {
|
|
495
|
+
console.log(chalk.yellow(`⚠️ Download completed with errors`));
|
|
496
|
+
return {
|
|
497
|
+
success: false,
|
|
498
|
+
filesDownloaded: succeeded,
|
|
499
|
+
failedFiles: failed,
|
|
500
|
+
isEmpty: false,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
488
503
|
} else {
|
|
489
504
|
console.log(
|
|
490
|
-
chalk.green(
|
|
505
|
+
chalk.green(`✅ All ${succeeded} files downloaded successfully!`)
|
|
491
506
|
);
|
|
507
|
+
console.log(chalk.green(`Folder cloned successfully!`));
|
|
508
|
+
return {
|
|
509
|
+
success: true,
|
|
510
|
+
filesDownloaded: succeeded,
|
|
511
|
+
failedFiles: failed,
|
|
512
|
+
isEmpty: false,
|
|
513
|
+
};
|
|
492
514
|
}
|
|
493
|
-
|
|
494
|
-
console.log(chalk.green(`Folder cloned successfully!`));
|
|
495
515
|
} catch (error) {
|
|
496
|
-
|
|
516
|
+
// Log the specific error details
|
|
517
|
+
console.error(chalk.red(`❌ Error downloading folder: ${error.message}`));
|
|
518
|
+
|
|
519
|
+
// Re-throw the error so the main CLI can exit with proper error code
|
|
520
|
+
throw error;
|
|
497
521
|
}
|
|
498
522
|
};
|
|
499
523
|
|
|
@@ -576,13 +600,16 @@ const downloadFolderWithResume = async (
|
|
|
576
600
|
|
|
577
601
|
try {
|
|
578
602
|
const contents = await fetchFolderContents(owner, repo, branch, folderPath);
|
|
579
|
-
|
|
580
603
|
if (!contents || contents.length === 0) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
604
|
+
const message = `No files found in ${folderPath || "repository root"}`;
|
|
605
|
+
console.log(chalk.yellow(message));
|
|
606
|
+
// Don't print success message when no files are found - this might indicate an error
|
|
607
|
+
return {
|
|
608
|
+
success: true,
|
|
609
|
+
filesDownloaded: 0,
|
|
610
|
+
failedFiles: 0,
|
|
611
|
+
isEmpty: true,
|
|
612
|
+
};
|
|
586
613
|
}
|
|
587
614
|
|
|
588
615
|
// Filter for blob type (files)
|
|
@@ -590,15 +617,18 @@ const downloadFolderWithResume = async (
|
|
|
590
617
|
const totalFiles = files.length;
|
|
591
618
|
|
|
592
619
|
if (totalFiles === 0) {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
620
|
+
const message = `No files found in ${
|
|
621
|
+
folderPath || "repository root"
|
|
622
|
+
} (only directories)`;
|
|
623
|
+
console.log(chalk.yellow(message));
|
|
624
|
+
// This is a legitimate case - directory exists but contains only subdirectories
|
|
625
|
+
console.log(chalk.green(`Directory structure downloaded successfully!`));
|
|
626
|
+
return {
|
|
627
|
+
success: true,
|
|
628
|
+
filesDownloaded: 0,
|
|
629
|
+
failedFiles: 0,
|
|
630
|
+
isEmpty: true,
|
|
631
|
+
};
|
|
602
632
|
}
|
|
603
633
|
|
|
604
634
|
// Create new checkpoint if none exists
|
|
@@ -748,7 +778,6 @@ const downloadFolderWithResume = async (
|
|
|
748
778
|
// Count results
|
|
749
779
|
const succeeded = checkpoint.downloadedFiles.length;
|
|
750
780
|
const failed = failedFiles.length;
|
|
751
|
-
|
|
752
781
|
if (failed > 0) {
|
|
753
782
|
console.log(
|
|
754
783
|
chalk.yellow(
|
|
@@ -766,21 +795,47 @@ const downloadFolderWithResume = async (
|
|
|
766
795
|
console.log(
|
|
767
796
|
chalk.blue(`💡 Run the same command again to retry failed downloads`)
|
|
768
797
|
);
|
|
798
|
+
|
|
799
|
+
// Don't claim success if files failed to download
|
|
800
|
+
if (succeeded === 0) {
|
|
801
|
+
console.log(
|
|
802
|
+
chalk.red(`❌ Download failed: No files were downloaded successfully`)
|
|
803
|
+
);
|
|
804
|
+
return {
|
|
805
|
+
success: false,
|
|
806
|
+
filesDownloaded: succeeded,
|
|
807
|
+
failedFiles: failed,
|
|
808
|
+
isEmpty: false,
|
|
809
|
+
};
|
|
810
|
+
} else {
|
|
811
|
+
console.log(chalk.yellow(`⚠️ Download completed with errors`));
|
|
812
|
+
return {
|
|
813
|
+
success: false,
|
|
814
|
+
filesDownloaded: succeeded,
|
|
815
|
+
failedFiles: failed,
|
|
816
|
+
isEmpty: false,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
769
819
|
} else {
|
|
770
820
|
console.log(
|
|
771
821
|
chalk.green(`🎉 All ${succeeded} files downloaded successfully!`)
|
|
772
822
|
);
|
|
773
823
|
resumeManager.cleanupCheckpoint(url, outputDir);
|
|
824
|
+
console.log(chalk.green(`Folder cloned successfully!`));
|
|
825
|
+
return {
|
|
826
|
+
success: true,
|
|
827
|
+
filesDownloaded: succeeded,
|
|
828
|
+
failedFiles: failed,
|
|
829
|
+
isEmpty: false,
|
|
830
|
+
};
|
|
774
831
|
}
|
|
775
|
-
|
|
776
|
-
console.log(chalk.green(`Folder cloned successfully!`));
|
|
777
832
|
} catch (error) {
|
|
778
833
|
// Save checkpoint on any error
|
|
779
834
|
if (checkpoint) {
|
|
780
835
|
resumeManager.saveCheckpoint(checkpoint);
|
|
781
836
|
}
|
|
782
837
|
|
|
783
|
-
console.error(chalk.red(
|
|
838
|
+
console.error(chalk.red(`❌ Error downloading folder: ${error.message}`));
|
|
784
839
|
throw error;
|
|
785
840
|
}
|
|
786
841
|
};
|
package/src/index.js
CHANGED
|
@@ -123,24 +123,46 @@ const initializeCLI = () => {
|
|
|
123
123
|
forceRestart: options.forceRestart || false,
|
|
124
124
|
};
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
} else {
|
|
130
|
-
console.log(`Downloading folder to: ${options.output}`);
|
|
126
|
+
let operationType = createArchive ? "archive" : "download";
|
|
127
|
+
let result = null;
|
|
128
|
+
let error = null;
|
|
131
129
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
downloadOptions
|
|
137
|
-
);
|
|
130
|
+
try {
|
|
131
|
+
if (createArchive) {
|
|
132
|
+
console.log(`Creating ZIP archive...`);
|
|
133
|
+
await downloadAndArchive(parsedUrl, options.output, archiveName);
|
|
138
134
|
} else {
|
|
139
|
-
|
|
135
|
+
console.log(`Downloading folder to: ${options.output}`);
|
|
136
|
+
if (downloadOptions.resume) {
|
|
137
|
+
result = await downloadFolderWithResume(
|
|
138
|
+
parsedUrl,
|
|
139
|
+
options.output,
|
|
140
|
+
downloadOptions
|
|
141
|
+
);
|
|
142
|
+
} else {
|
|
143
|
+
result = await downloadFolder(parsedUrl, options.output);
|
|
144
|
+
}
|
|
140
145
|
}
|
|
146
|
+
} catch (opError) {
|
|
147
|
+
error = opError;
|
|
141
148
|
}
|
|
142
149
|
|
|
143
|
-
|
|
150
|
+
// Consolidated result and error handling
|
|
151
|
+
if (error) {
|
|
152
|
+
const failMsg =
|
|
153
|
+
operationType === "archive"
|
|
154
|
+
? `❌ Archive creation failed: ${error.message}`
|
|
155
|
+
: `❌ Download failed: ${error.message}`;
|
|
156
|
+
console.error(chalk.red(failMsg));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
} else if (!createArchive && result && !result.success) {
|
|
159
|
+
console.error(chalk.red(`❌ Download failed`));
|
|
160
|
+
process.exit(1);
|
|
161
|
+
} else if (!createArchive && result && result.isEmpty) {
|
|
162
|
+
console.log("Operation completed - no files to download!");
|
|
163
|
+
} else {
|
|
164
|
+
console.log("Operation completed successfully!");
|
|
165
|
+
}
|
|
144
166
|
} catch (error) {
|
|
145
167
|
console.error("Error:", error.message);
|
|
146
168
|
process.exit(1);
|