git-ripper 1.3.1 → 1.3.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.3.1",
3
+ "version": "1.3.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",
@@ -31,9 +31,11 @@
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
33
  "axios": "^1.6.7",
34
+ "chalk": "^5.3.0",
34
35
  "cli-progress": "^3.12.0",
35
36
  "commander": "^12.0.0",
36
- "p-limit": "^6.2.0"
37
+ "p-limit": "^6.2.0",
38
+ "pretty-bytes": "^6.1.1"
37
39
  },
38
40
  "repository": {
39
41
  "type": "git",
package/src/downloader.js CHANGED
@@ -5,6 +5,8 @@ import { fileURLToPath } from "url";
5
5
  import { dirname } from "path";
6
6
  import cliProgress from "cli-progress";
7
7
  import pLimit from "p-limit";
8
+ import chalk from "chalk";
9
+ import prettyBytes from "pretty-bytes";
8
10
 
9
11
  // Set concurrency limit (adjustable based on network performance)
10
12
  const limit = pLimit(500);
@@ -13,6 +15,27 @@ const limit = pLimit(500);
13
15
  const __filename = fileURLToPath(import.meta.url);
14
16
  const __dirname = dirname(__filename);
15
17
 
18
+ // Define spinner animation frames
19
+ const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
20
+ // Alternative progress bar characters for more visual appeal
21
+ const progressChars = {
22
+ complete: '▰', // Alternative: '■', '●', '◆', '▣'
23
+ incomplete: '▱', // Alternative: '□', '○', '◇', '▢'
24
+ };
25
+
26
+ // Track frame index for spinner animation
27
+ let spinnerFrameIndex = 0;
28
+
29
+ /**
30
+ * Returns the next spinner frame for animation
31
+ * @returns {string} - The spinner character
32
+ */
33
+ const getSpinnerFrame = () => {
34
+ const frame = spinnerFrames[spinnerFrameIndex];
35
+ spinnerFrameIndex = (spinnerFrameIndex + 1) % spinnerFrames.length;
36
+ return frame;
37
+ };
38
+
16
39
  /**
17
40
  * Fetches the contents of a folder from a GitHub repository
18
41
  * @param {string} owner - Repository owner
@@ -53,12 +76,68 @@ const downloadFile = async (owner, repo, branch, filePath, outputPath) => {
53
76
  const response = await axios.get(url, { responseType: "arraybuffer" });
54
77
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
55
78
  fs.writeFileSync(outputPath, Buffer.from(response.data));
56
- return { filePath, success: true };
79
+ return {
80
+ filePath,
81
+ success: true,
82
+ size: response.data.length
83
+ };
57
84
  } catch (error) {
58
- return { filePath, success: false, error: error.message };
85
+ return {
86
+ filePath,
87
+ success: false,
88
+ error: error.message,
89
+ size: 0
90
+ };
59
91
  }
60
92
  };
61
93
 
94
+ /**
95
+ * Creates a simplified progress bar renderer with animation
96
+ * @param {string} owner - Repository owner
97
+ * @param {string} repo - Repository name
98
+ * @param {string} folderPath - Path to the folder
99
+ * @returns {Function} - Function to render progress bar
100
+ */
101
+ const createProgressRenderer = (owner, repo, folderPath) => {
102
+ // Default terminal width
103
+ const terminalWidth = process.stdout.columns || 80;
104
+
105
+ return (options, params, payload) => {
106
+ try {
107
+ const { value, total, startTime } = params;
108
+ const { downloadedSize = 0 } = payload || { downloadedSize: 0 };
109
+
110
+ // Calculate progress percentage
111
+ const progress = Math.min(1, Math.max(0, value / Math.max(1, total)));
112
+ const percentage = Math.floor(progress * 100);
113
+
114
+ // Calculate elapsed time
115
+ const elapsedSecs = Math.max(0.1, (Date.now() - startTime) / 1000);
116
+
117
+ // Create the progress bar
118
+ const barLength = Math.max(20, Math.min(40, Math.floor(terminalWidth / 2)));
119
+ const completedLength = Math.round(barLength * progress);
120
+ const remainingLength = barLength - completedLength;
121
+
122
+ // Build the bar with custom progress characters
123
+ const completedBar = chalk.greenBright(progressChars.complete.repeat(completedLength));
124
+ const remainingBar = chalk.gray(progressChars.incomplete.repeat(remainingLength));
125
+
126
+ // Add spinner for animation
127
+ const spinner = chalk.cyanBright(getSpinnerFrame());
128
+
129
+ // Format the output
130
+ const progressInfo = `${chalk.cyan(`${value}/${total}`)} files`;
131
+ const sizeInfo = prettyBytes(downloadedSize || 0);
132
+
133
+ return `${spinner} ${completedBar}${remainingBar} ${chalk.yellow(percentage + '%')} | ${progressInfo} | ${chalk.magenta(sizeInfo)}`;
134
+ } catch (error) {
135
+ // Fallback to a very simple progress indicator
136
+ return `${Math.floor((params.value / params.total) * 100)}% complete`;
137
+ }
138
+ };
139
+ };
140
+
62
141
  /**
63
142
  * Downloads all files from a folder in a GitHub repository
64
143
  * @param {Object} repoInfo - Object containing repository information
@@ -70,47 +149,91 @@ const downloadFile = async (owner, repo, branch, filePath, outputPath) => {
70
149
  * @returns {Promise<void>} - Promise that resolves when all files are downloaded
71
150
  */
72
151
  const downloadFolder = async ({ owner, repo, branch, folderPath }, outputDir) => {
73
- console.log(`Cloning ${folderPath} from ${owner}/${repo} (${branch})...`);
152
+ console.log(chalk.cyan(`Analyzing repository structure for ${owner}/${repo}...`));
74
153
 
75
- const contents = await fetchFolderContents(owner, repo, branch, folderPath);
76
-
77
- if (contents.length === 0) {
78
- console.log(`No files found in ${folderPath}`);
79
- return;
80
- }
154
+ try {
155
+ const contents = await fetchFolderContents(owner, repo, branch, folderPath);
156
+
157
+ if (!contents || contents.length === 0) {
158
+ console.log(chalk.yellow(`No files found in ${folderPath || 'repository root'}`));
159
+ console.log(chalk.green(`Folder cloned successfully!`));
160
+ return;
161
+ }
81
162
 
82
- let totalFiles = contents.filter(item => item.type === "blob").length;
83
- console.log(`Preparing to download ${totalFiles} files/folders...`);
84
-
85
- // Progress bar setup
86
- const bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
87
- bar.start(totalFiles, 0);
88
-
89
- // Create download promises with concurrency control
90
- const fileDownloadPromises = contents
91
- .filter((item) => item.type === "blob")
92
- .map((item) => {
163
+ const files = contents.filter(item => item.type === "blob");
164
+ const totalFiles = files.length;
165
+
166
+ console.log(chalk.cyan(`Downloading ${totalFiles} files from ${chalk.white(owner + '/' + repo)}...`));
167
+
168
+ // Simplified progress bar setup
169
+ const progressBar = new cliProgress.SingleBar({
170
+ format: createProgressRenderer(owner, repo, folderPath),
171
+ hideCursor: true,
172
+ clearOnComplete: false,
173
+ stopOnComplete: true,
174
+ forceRedraw: true
175
+ });
176
+
177
+ // Track download metrics
178
+ let downloadedSize = 0;
179
+ const startTime = Date.now();
180
+
181
+ // Start progress bar
182
+ progressBar.start(totalFiles, 0, {
183
+ downloadedSize: 0,
184
+ startTime
185
+ });
186
+
187
+ // Create download promises with concurrency control
188
+ const fileDownloadPromises = files.map((item) => {
93
189
  // Keep the original structure by preserving the folder name
94
- // For a path like "src/components/Button.js" relative to "src", store as "components/Button.js"
95
- const relativePath = item.path.substring(folderPath.length).replace(/^\//, "");
190
+ let relativePath = item.path;
191
+ if (folderPath && folderPath.trim() !== '') {
192
+ relativePath = item.path.substring(folderPath.length).replace(/^\//, "");
193
+ }
96
194
  const outputFilePath = path.join(outputDir, relativePath);
97
195
 
98
196
  return limit(async () => {
99
- const result = await downloadFile(owner, repo, branch, item.path, outputFilePath);
100
- bar.increment(); // Update progress bar
101
- return result;
197
+ try {
198
+ const result = await downloadFile(owner, repo, branch, item.path, outputFilePath);
199
+
200
+ // Update progress metrics
201
+ if (result.success) {
202
+ downloadedSize += (result.size || 0);
203
+ }
204
+
205
+ // Update progress bar with current metrics
206
+ progressBar.increment(1, {
207
+ downloadedSize
208
+ });
209
+
210
+ return result;
211
+ } catch (error) {
212
+ return { filePath: item.path, success: false, error: error.message, size: 0 };
213
+ }
102
214
  });
103
215
  });
104
216
 
105
- // Execute downloads in parallel
106
- const results = await Promise.all(fileDownloadPromises);
107
- bar.stop(); // Stop progress bar
217
+ // Execute downloads in parallel
218
+ const results = await Promise.all(fileDownloadPromises);
219
+ progressBar.stop();
220
+
221
+ console.log(); // Add an empty line after progress bar
108
222
 
109
- // Count successful and failed downloads
110
- const succeeded = results.filter((r) => r.success).length;
111
- const failed = results.filter((r) => !r.success).length;
223
+ // Count successful and failed downloads
224
+ const succeeded = results.filter((r) => r.success).length;
225
+ const failed = results.filter((r) => !r.success).length;
112
226
 
113
- console.log(`Downloaded ${succeeded} files successfully${failed > 0 ? `, ${failed} files failed` : ""}`);
227
+ if (failed > 0) {
228
+ console.log(chalk.yellow(`Downloaded ${succeeded} files successfully, ${failed} files failed`));
229
+ } else {
230
+ console.log(chalk.green(` All ${succeeded} files downloaded successfully!`));
231
+ }
232
+
233
+ console.log(chalk.green(`Folder cloned successfully!`));
234
+ } catch (error) {
235
+ console.error(chalk.red(`Error downloading folder: ${error.message}`));
236
+ }
114
237
  };
115
238
 
116
239
  // Export functions in ESM format
package/src/index.js CHANGED
@@ -4,7 +4,7 @@ import { downloadFolder } from './downloader.js';
4
4
 
5
5
  const initializeCLI = () => {
6
6
  program
7
- .version('1.3.1')
7
+ .version('1.3.6')
8
8
  .description('Clone specific folders from GitHub repositories')
9
9
  .argument('<url>', 'GitHub URL of the folder to clone')
10
10
  .option('-o, --output <directory>', 'Output directory', process.cwd())