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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-ripper",
3
- "version": "1.4.5",
3
+ "version": "1.4.6",
4
4
  "description": "CLI tool that lets you download specific folders from GitHub repositories without cloning the entire repo.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
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
- console.error(
59
- chalk.red(
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
- console.error(
72
- chalk.red(
73
- `Failed to fetch default branch for ${owner}/${repo}: ${error.message}`
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
- console.error(
118
- chalk.red(
119
- `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).`
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
- console.error(
126
- chalk.red(
127
- `Access forbidden: ${
128
- error.response.data.message || "Unknown reason"
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
- console.error(
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
- console.error(
143
- chalk.red(
144
- `API error (${error.response.status}): ${
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
- console.error(
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
- console.error(chalk.red(`Error preparing request: ${error.message}`));
140
+ errorMessage = `Error preparing request: ${error.message}`;
158
141
  }
159
- return [];
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
- console.log(
353
- chalk.yellow(`No files found in ${folderPath || "repository root"}`)
354
- );
355
- console.log(chalk.green(`Folder cloned successfully!`));
356
- return;
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
- console.log(
365
- chalk.yellow(
366
- `No files found in ${
367
- folderPath || "repository root"
368
- } (only directories)`
369
- )
370
- );
371
- console.log(chalk.green(`Folder cloned successfully!`));
372
- return;
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(` All ${succeeded} files downloaded successfully!`)
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
- console.error(chalk.red(`Error downloading folder: ${error.message}`));
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
- console.log(
582
- chalk.yellow(`No files found in ${folderPath || "repository root"}`)
583
- );
584
- console.log(chalk.green(`Folder cloned successfully!`));
585
- return;
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
- console.log(
594
- chalk.yellow(
595
- `No files found in ${
596
- folderPath || "repository root"
597
- } (only directories)`
598
- )
599
- );
600
- console.log(chalk.green(`Folder cloned successfully!`));
601
- return;
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(`Error downloading folder: ${error.message}`));
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
- if (createArchive) {
127
- console.log(`Creating ZIP archive...`);
128
- await downloadAndArchive(parsedUrl, options.output, archiveName);
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
- if (downloadOptions.resume) {
133
- await downloadFolderWithResume(
134
- parsedUrl,
135
- options.output,
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
- await downloadFolder(parsedUrl, options.output);
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
- console.log("Operation completed successfully!");
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);