git-ripper 1.3.6 → 1.4.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
@@ -31,6 +31,7 @@ Have you ever needed just a single component from a massive repository? Or wante
31
31
  - **Directory Structure**: Preserves complete folder structure
32
32
  - **Custom Output**: Specify your preferred output directory
33
33
  - **Branch Support**: Works with any branch, not just the default one
34
+ - **Archive Export**: Create ZIP or TAR archives of downloaded content
34
35
  - **Simple Interface**: Clean, intuitive command-line experience
35
36
  - **Lightweight**: Minimal dependencies and fast execution
36
37
  - **No Authentication**: Works with public repositories without requiring credentials
@@ -67,11 +68,26 @@ git-ripper https://github.com/username/repository/tree/branch/folder
67
68
  git-ripper https://github.com/username/repository/tree/branch/folder -o ./my-output-folder
68
69
  ```
69
70
 
71
+ ### Creating ZIP Archive
72
+
73
+ ```bash
74
+ git-ripper https://github.com/username/repository/tree/branch/folder --zip
75
+ ```
76
+
77
+ ### Creating TAR Archive with Custom Name
78
+
79
+ ```bash
80
+ git-ripper https://github.com/username/repository/tree/branch/folder --tar="my-archive.tar"
81
+ ```
82
+
70
83
  ### Command Line Options
71
84
 
72
85
  | Option | Description | Default |
73
86
  |--------|-------------|---------|
74
87
  | `-o, --output <directory>` | Specify output directory | Current directory |
88
+ | `--zip [filename]` | Create ZIP archive of downloaded content | - |
89
+ | `--tar [filename]` | Create TAR archive of downloaded content | - |
90
+ | `--compression-level <level>` | Set compression level (1-9) | 6 |
75
91
  | `-V, --version` | Show version number | - |
76
92
  | `-h, --help` | Show help | - |
77
93
 
@@ -105,6 +121,16 @@ git-ripper https://github.com/nodejs/node/tree/main/doc -o ./node-docs
105
121
  git-ripper https://github.com/tailwindlabs/tailwindcss/tree/master/src/components -o ./tailwind-components
106
122
  ```
107
123
 
124
+ ### Download and Create Archive
125
+
126
+ ```bash
127
+ # Download React DOM package and create a ZIP archive
128
+ git-ripper https://github.com/facebook/react/tree/main/packages/react-dom --zip
129
+
130
+ # Extract VS Code build configuration with maximum compression
131
+ git-ripper https://github.com/microsoft/vscode/tree/main/build --tar --compression-level=9
132
+ ```
133
+
108
134
  ## How It Works
109
135
 
110
136
  Git-ripper operates in four stages:
@@ -112,7 +138,7 @@ Git-ripper operates in four stages:
112
138
  1. **URL Parsing**: Extracts repository owner, name, branch, and target folder path
113
139
  2. **API Request**: Uses GitHub's API to fetch the folder structure
114
140
  3. **Content Download**: Retrieves each file individually while maintaining directory structure
115
- 4. **Local Storage**: Saves files to your specified output directory
141
+ 4. **Local Storage or Archiving**: Saves files to your specified output directory or creates an archive
116
142
 
117
143
  ## Configuration
118
144
 
@@ -160,6 +186,7 @@ See the [open issues](https://github.com/sairajB/git-ripper/issues) for a list o
160
186
 
161
187
  ## Roadmap
162
188
 
189
+ - [x] Add archive export options (ZIP/TAR)
163
190
  - [ ] Add GitHub token authentication
164
191
  - [ ] Support for GitLab and Bitbucket repositories
165
192
  - [ ] Download from specific commits or tags
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-ripper",
3
- "version": "1.3.6",
3
+ "version": "1.4.0",
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",
@@ -30,6 +30,7 @@
30
30
  "author": "sairajb",
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
+ "archiver": "^6.0.1",
33
34
  "axios": "^1.6.7",
34
35
  "chalk": "^5.3.0",
35
36
  "cli-progress": "^3.12.0",
@@ -0,0 +1,186 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import archiver from 'archiver';
4
+ import chalk from 'chalk';
5
+
6
+ /**
7
+ * Validates the output path for an archive file
8
+ * @param {string} outputPath - Path where the archive should be saved
9
+ * @returns {boolean} - True if the path is valid, throws an error otherwise
10
+ * @throws {Error} - If the output path is invalid
11
+ */
12
+ const validateArchivePath = (outputPath) => {
13
+ // Check if path is provided
14
+ if (!outputPath) {
15
+ throw new Error('Output path is required');
16
+ }
17
+
18
+ // Check if the output directory exists or can be created
19
+ const outputDir = path.dirname(outputPath);
20
+ try {
21
+ if (!fs.existsSync(outputDir)) {
22
+ fs.mkdirSync(outputDir, { recursive: true });
23
+ }
24
+
25
+ // Check if the directory is writable
26
+ fs.accessSync(outputDir, fs.constants.W_OK);
27
+
28
+ // Check if file already exists and is writable
29
+ if (fs.existsSync(outputPath)) {
30
+ fs.accessSync(outputPath, fs.constants.W_OK);
31
+ // File exists and is writable, so we'll overwrite it
32
+ console.warn(chalk.yellow(`Warning: File ${outputPath} already exists and will be overwritten`));
33
+ }
34
+
35
+ return true;
36
+ } catch (error) {
37
+ if (error.code === 'EACCES') {
38
+ throw new Error(`Permission denied: Cannot write to ${outputPath}`);
39
+ }
40
+ throw new Error(`Invalid output path: ${error.message}`);
41
+ }
42
+ };
43
+
44
+ /**
45
+ * Creates an archive (zip or tar) from a directory
46
+ *
47
+ * @param {string} sourceDir - Source directory to archive
48
+ * @param {string} outputPath - Path where the archive should be saved
49
+ * @param {object} options - Archive options
50
+ * @param {string} options.format - Archive format ('zip' or 'tar')
51
+ * @param {number} options.compressionLevel - Compression level (0-9, default: 6)
52
+ * @returns {Promise<string>} - Path to the created archive
53
+ */
54
+ export const createArchive = (sourceDir, outputPath, options = {}) => {
55
+ return new Promise((resolve, reject) => {
56
+ try {
57
+ const { format = 'zip', compressionLevel = 6 } = options;
58
+
59
+ // Validate source directory
60
+ if (!fs.existsSync(sourceDir)) {
61
+ return reject(new Error(`Source directory does not exist: ${sourceDir}`));
62
+ }
63
+
64
+ const stats = fs.statSync(sourceDir);
65
+ if (!stats.isDirectory()) {
66
+ return reject(new Error(`Source path is not a directory: ${sourceDir}`));
67
+ }
68
+
69
+ // Validate output path
70
+ validateArchivePath(outputPath);
71
+
72
+ // Ensure the output directory exists
73
+ const outputDir = path.dirname(outputPath);
74
+ if (!fs.existsSync(outputDir)) {
75
+ fs.mkdirSync(outputDir, { recursive: true });
76
+ }
77
+
78
+ // Create output stream
79
+ const output = fs.createWriteStream(outputPath);
80
+ let archive;
81
+
82
+ // Create the appropriate archive type
83
+ if (format === 'zip') {
84
+ archive = archiver('zip', {
85
+ zlib: { level: compressionLevel }
86
+ });
87
+ } else if (format === 'tar') {
88
+ archive = archiver('tar');
89
+ // Use gzip compression for tar if compressionLevel > 0
90
+ if (compressionLevel > 0) {
91
+ archive = archiver('tar', {
92
+ gzip: true,
93
+ gzipOptions: { level: compressionLevel }
94
+ });
95
+ }
96
+ } else {
97
+ return reject(new Error(`Unsupported archive format: ${format}`));
98
+ }
99
+
100
+ // Listen for archive events
101
+ output.on('close', () => {
102
+ const size = archive.pointer();
103
+ console.log(chalk.green(`✓ Archive created: ${outputPath} (${(size / 1024 / 1024).toFixed(2)} MB)`));
104
+ resolve(outputPath);
105
+ });
106
+
107
+ archive.on('error', (err) => {
108
+ reject(err);
109
+ });
110
+
111
+ archive.on('warning', (err) => {
112
+ if (err.code === 'ENOENT') {
113
+ console.warn(chalk.yellow(`Warning: ${err.message}`));
114
+ } else {
115
+ reject(err);
116
+ }
117
+ });
118
+
119
+ // Pipe archive data to the output file
120
+ archive.pipe(output);
121
+
122
+ // Add the directory contents to the archive
123
+ archive.directory(sourceDir, false);
124
+
125
+ // Finalize the archive
126
+ archive.finalize();
127
+ } catch (error) {
128
+ reject(error);
129
+ }
130
+ });
131
+ };
132
+
133
+ /**
134
+ * Downloads folder contents and creates an archive
135
+ *
136
+ * @param {object} repoInfo - Repository information object
137
+ * @param {string} outputDir - Directory where files should be downloaded
138
+ * @param {string} archiveFormat - Archive format ('zip' or 'tar')
139
+ * @param {string} archiveName - Custom name for the archive file
140
+ * @param {number} compressionLevel - Compression level (0-9)
141
+ * @returns {Promise<string>} - Path to the created archive
142
+ */
143
+ export const downloadAndArchive = async (repoInfo, outputDir, archiveFormat = 'zip', archiveName = null, compressionLevel = 6) => {
144
+ const { downloadFolder } = await import('./downloader.js');
145
+
146
+ console.log(chalk.cyan(`Downloading folder and preparing to create ${archiveFormat.toUpperCase()} archive...`));
147
+
148
+ // Create a temporary directory for the download
149
+ const tempDir = path.join(outputDir, `.temp-${Date.now()}`);
150
+ fs.mkdirSync(tempDir, { recursive: true });
151
+
152
+ try {
153
+ // Download the folder contents
154
+ await downloadFolder(repoInfo, tempDir);
155
+
156
+ // Determine archive filename
157
+ let archiveFileName = archiveName;
158
+ if (!archiveFileName) {
159
+ const { owner, repo, folderPath } = repoInfo;
160
+ const folderName = folderPath ? folderPath.split('/').pop() : repo;
161
+ archiveFileName = `${folderName || repo}-${owner}`;
162
+ }
163
+
164
+ // Add extension if not present
165
+ if (!archiveFileName.endsWith(`.${archiveFormat}`)) {
166
+ archiveFileName += `.${archiveFormat}`;
167
+ }
168
+
169
+ const archivePath = path.join(outputDir, archiveFileName);
170
+
171
+ // Create the archive
172
+ console.log(chalk.cyan(`Creating ${archiveFormat.toUpperCase()} archive...`));
173
+ await createArchive(tempDir, archivePath, { format: archiveFormat, compressionLevel });
174
+
175
+ return archivePath;
176
+ } catch (error) {
177
+ throw new Error(`Failed to create archive: ${error.message}`);
178
+ } finally {
179
+ // Clean up temporary directory
180
+ try {
181
+ fs.rmSync(tempDir, { recursive: true, force: true });
182
+ } catch (err) {
183
+ console.warn(chalk.yellow(`Warning: Failed to clean up temporary directory: ${err.message}`));
184
+ }
185
+ }
186
+ };
package/src/downloader.js CHANGED
@@ -9,7 +9,8 @@ import chalk from "chalk";
9
9
  import prettyBytes from "pretty-bytes";
10
10
 
11
11
  // Set concurrency limit (adjustable based on network performance)
12
- const limit = pLimit(500);
12
+ // Reduced from 500 to 5 to prevent GitHub API rate limiting
13
+ const limit = pLimit(5);
13
14
 
14
15
  // Ensure __dirname and __filename are available in ESM
15
16
  const __filename = fileURLToPath(import.meta.url);
@@ -49,13 +50,42 @@ const fetchFolderContents = async (owner, repo, branch, folderPath) => {
49
50
 
50
51
  try {
51
52
  const response = await axios.get(apiUrl);
53
+
54
+ // Check if GitHub API returned truncated results
55
+ if (response.data.truncated) {
56
+ console.warn(chalk.yellow(
57
+ `Warning: The repository is too large and some files may be missing. ` +
58
+ `Consider using git clone for complete repositories.`
59
+ ));
60
+ }
61
+
52
62
  return response.data.tree.filter((item) => item.path.startsWith(folderPath));
53
63
  } catch (error) {
54
- if (error.response && error.response.status === 404) {
55
- console.error(`Repository, branch, or folder not found: ${owner}/${repo}/${branch}/${folderPath}`);
56
- return [];
64
+ if (error.response) {
65
+ // Handle specific HTTP error codes
66
+ switch(error.response.status) {
67
+ case 403:
68
+ if (error.response.headers['x-ratelimit-remaining'] === '0') {
69
+ console.error(chalk.red(
70
+ `GitHub API rate limit exceeded. Please wait until ${
71
+ new Date(parseInt(error.response.headers['x-ratelimit-reset']) * 1000).toLocaleTimeString()
72
+ } or add a GitHub token (feature coming soon).`
73
+ ));
74
+ } else {
75
+ console.error(chalk.red(`Access forbidden: ${error.response.data.message || 'Unknown reason'}`));
76
+ }
77
+ break;
78
+ case 404:
79
+ console.error(chalk.red(`Repository, branch, or folder not found: ${owner}/${repo}/${branch}/${folderPath}`));
80
+ break;
81
+ default:
82
+ console.error(chalk.red(`API error (${error.response.status}): ${error.response.data.message || error.message}`));
83
+ }
84
+ } else if (error.request) {
85
+ console.error(chalk.red(`Network error: No response received from GitHub. Please check your internet connection.`));
86
+ } else {
87
+ console.error(chalk.red(`Error preparing request: ${error.message}`));
57
88
  }
58
- console.error(`Failed to fetch folder contents: ${error.message}`);
59
89
  return [];
60
90
  }
61
91
  };
@@ -74,18 +104,61 @@ const downloadFile = async (owner, repo, branch, filePath, outputPath) => {
74
104
 
75
105
  try {
76
106
  const response = await axios.get(url, { responseType: "arraybuffer" });
77
- fs.mkdirSync(path.dirname(outputPath), { recursive: true });
78
- fs.writeFileSync(outputPath, Buffer.from(response.data));
107
+
108
+ // Ensure the directory exists
109
+ try {
110
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
111
+ } catch (dirError) {
112
+ return {
113
+ filePath,
114
+ success: false,
115
+ error: `Failed to create directory: ${dirError.message}`,
116
+ size: 0
117
+ };
118
+ }
119
+
120
+ // Write the file
121
+ try {
122
+ fs.writeFileSync(outputPath, Buffer.from(response.data));
123
+ } catch (fileError) {
124
+ return {
125
+ filePath,
126
+ success: false,
127
+ error: `Failed to write file: ${fileError.message}`,
128
+ size: 0
129
+ };
130
+ }
131
+
79
132
  return {
80
133
  filePath,
81
134
  success: true,
82
135
  size: response.data.length
83
136
  };
84
137
  } catch (error) {
138
+ // More detailed error handling for network requests
139
+ let errorMessage = error.message;
140
+
141
+ if (error.response) {
142
+ // The request was made and the server responded with an error status
143
+ switch (error.response.status) {
144
+ case 403:
145
+ errorMessage = "Access forbidden (possibly rate limited)";
146
+ break;
147
+ case 404:
148
+ errorMessage = "File not found";
149
+ break;
150
+ default:
151
+ errorMessage = `HTTP error ${error.response.status}`;
152
+ }
153
+ } else if (error.request) {
154
+ // The request was made but no response was received
155
+ errorMessage = "No response from server";
156
+ }
157
+
85
158
  return {
86
159
  filePath,
87
160
  success: false,
88
- error: error.message,
161
+ error: errorMessage,
89
162
  size: 0
90
163
  };
91
164
  }
@@ -160,9 +233,16 @@ const downloadFolder = async ({ owner, repo, branch, folderPath }, outputDir) =>
160
233
  return;
161
234
  }
162
235
 
236
+ // Filter for blob type (files)
163
237
  const files = contents.filter(item => item.type === "blob");
164
238
  const totalFiles = files.length;
165
239
 
240
+ if (totalFiles === 0) {
241
+ console.log(chalk.yellow(`No files found in ${folderPath || 'repository root'} (only directories)`));
242
+ console.log(chalk.green(`Folder cloned successfully!`));
243
+ return;
244
+ }
245
+
166
246
  console.log(chalk.cyan(`Downloading ${totalFiles} files from ${chalk.white(owner + '/' + repo)}...`));
167
247
 
168
248
  // Simplified progress bar setup
@@ -177,6 +257,7 @@ const downloadFolder = async ({ owner, repo, branch, folderPath }, outputDir) =>
177
257
  // Track download metrics
178
258
  let downloadedSize = 0;
179
259
  const startTime = Date.now();
260
+ let failedFiles = [];
180
261
 
181
262
  // Start progress bar
182
263
  progressBar.start(totalFiles, 0, {
@@ -200,6 +281,12 @@ const downloadFolder = async ({ owner, repo, branch, folderPath }, outputDir) =>
200
281
  // Update progress metrics
201
282
  if (result.success) {
202
283
  downloadedSize += (result.size || 0);
284
+ } else {
285
+ // Track failed files for reporting
286
+ failedFiles.push({
287
+ path: item.path,
288
+ error: result.error
289
+ });
203
290
  }
204
291
 
205
292
  // Update progress bar with current metrics
@@ -209,12 +296,18 @@ const downloadFolder = async ({ owner, repo, branch, folderPath }, outputDir) =>
209
296
 
210
297
  return result;
211
298
  } catch (error) {
299
+ failedFiles.push({
300
+ path: item.path,
301
+ error: error.message
302
+ });
303
+
304
+ progressBar.increment(1, { downloadedSize });
212
305
  return { filePath: item.path, success: false, error: error.message, size: 0 };
213
306
  }
214
307
  });
215
308
  });
216
309
 
217
- // Execute downloads in parallel
310
+ // Execute downloads in parallel with controlled concurrency
218
311
  const results = await Promise.all(fileDownloadPromises);
219
312
  progressBar.stop();
220
313
 
@@ -222,10 +315,20 @@ const downloadFolder = async ({ owner, repo, branch, folderPath }, outputDir) =>
222
315
 
223
316
  // Count successful and failed downloads
224
317
  const succeeded = results.filter((r) => r.success).length;
225
- const failed = results.filter((r) => !r.success).length;
318
+ const failed = failedFiles.length;
226
319
 
227
320
  if (failed > 0) {
228
321
  console.log(chalk.yellow(`Downloaded ${succeeded} files successfully, ${failed} files failed`));
322
+
323
+ // Show detailed errors if there aren't too many
324
+ if (failed <= 5) {
325
+ console.log(chalk.yellow('Failed files:'));
326
+ failedFiles.forEach(file => {
327
+ console.log(chalk.yellow(` - ${file.path}: ${file.error}`));
328
+ });
329
+ } else {
330
+ console.log(chalk.yellow(`${failed} files failed to download. Check your connection or repository access.`));
331
+ }
229
332
  } else {
230
333
  console.log(chalk.green(` All ${succeeded} files downloaded successfully!`));
231
334
  }
package/src/index.js CHANGED
@@ -1,23 +1,103 @@
1
1
  import { program } from 'commander';
2
2
  import { parseGitHubUrl } from './parser.js';
3
3
  import { downloadFolder } from './downloader.js';
4
+ import { downloadAndArchive } from './archiver.js';
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname, join, resolve } from 'path';
7
+ import fs from 'fs';
8
+
9
+ // Get package.json for version
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ const packagePath = join(__dirname, '..', 'package.json');
13
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
14
+
15
+ /**
16
+ * Validates and ensures the output directory exists
17
+ * @param {string} outputDir - The directory path to validate
18
+ * @returns {string} - The resolved directory path
19
+ * @throws {Error} - If the directory is invalid or cannot be created
20
+ */
21
+ const validateOutputDirectory = (outputDir) => {
22
+ if (!outputDir) {
23
+ throw new Error('Output directory is required');
24
+ }
25
+
26
+ // Resolve to absolute path
27
+ const resolvedDir = resolve(outputDir);
28
+
29
+ try {
30
+ // Check if directory exists, if not try to create it
31
+ if (!fs.existsSync(resolvedDir)) {
32
+ fs.mkdirSync(resolvedDir, { recursive: true });
33
+ } else {
34
+ // Check if it's actually a directory
35
+ const stats = fs.statSync(resolvedDir);
36
+ if (!stats.isDirectory()) {
37
+ throw new Error(`Output path exists but is not a directory: ${outputDir}`);
38
+ }
39
+ }
40
+
41
+ // Check if the directory is writable
42
+ fs.accessSync(resolvedDir, fs.constants.W_OK);
43
+
44
+ return resolvedDir;
45
+ } catch (error) {
46
+ if (error.code === 'EACCES') {
47
+ throw new Error(`Permission denied: Cannot write to ${outputDir}`);
48
+ }
49
+ throw new Error(`Invalid output directory: ${error.message}`);
50
+ }
51
+ };
4
52
 
5
53
  const initializeCLI = () => {
6
54
  program
7
- .version('1.3.6')
55
+ .version(packageJson.version)
8
56
  .description('Clone specific folders from GitHub repositories')
9
57
  .argument('<url>', 'GitHub URL of the folder to clone')
10
58
  .option('-o, --output <directory>', 'Output directory', process.cwd())
59
+ .option('--zip [filename]', 'Create ZIP archive of downloaded files')
60
+ .option('--tar [filename]', 'Create TAR archive of downloaded files')
61
+ .option('--compression-level <level>', 'Compression level (1-9)', '6')
11
62
  .action(async (url, options) => {
12
63
  try {
13
64
  console.log(`Parsing URL: ${url}`);
14
65
  const parsedUrl = parseGitHubUrl(url);
15
- console.log(`Parsed URL:`, parsedUrl);
66
+
67
+ // Validate options
68
+ if (options.compressionLevel) {
69
+ const level = parseInt(options.compressionLevel, 10);
70
+ if (isNaN(level) || level < 1 || level > 9) {
71
+ throw new Error('Compression level must be a number between 1 and 9');
72
+ }
73
+ }
16
74
 
17
- console.log(`Downloading folder to: ${options.output}`);
18
- await downloadFolder(parsedUrl, options.output);
75
+ if (options.zip && options.tar) {
76
+ throw new Error('Cannot specify both --zip and --tar options at the same time');
77
+ }
78
+
79
+ // Validate output directory
80
+ try {
81
+ options.output = validateOutputDirectory(options.output);
82
+ } catch (dirError) {
83
+ throw new Error(`Output directory error: ${dirError.message}`);
84
+ }
85
+
86
+ // Handle archive options
87
+ const archiveFormat = options.zip ? 'zip' : options.tar ? 'tar' : null;
88
+ const archiveName = typeof options.zip === 'string' ? options.zip :
89
+ typeof options.tar === 'string' ? options.tar : null;
90
+ const compressionLevel = parseInt(options.compressionLevel, 10) || 6;
91
+
92
+ if (archiveFormat) {
93
+ console.log(`Creating ${archiveFormat.toUpperCase()} archive...`);
94
+ await downloadAndArchive(parsedUrl, options.output, archiveFormat, archiveName, compressionLevel);
95
+ } else {
96
+ console.log(`Downloading folder to: ${options.output}`);
97
+ await downloadFolder(parsedUrl, options.output);
98
+ }
19
99
 
20
- console.log('Folder cloned successfully!');
100
+ console.log('Operation completed successfully!');
21
101
  } catch (error) {
22
102
  console.error('Error:', error.message);
23
103
  process.exit(1);
@@ -32,5 +112,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
32
112
  initializeCLI();
33
113
  }
34
114
 
35
- // ✅ Fix the incorrect export
36
115
  export { initializeCLI, downloadFolder };
package/src/parser.js CHANGED
@@ -1,8 +1,27 @@
1
1
  export function parseGitHubUrl(url) {
2
- const urlParts = url.split('/');
3
- const owner = urlParts[3];
4
- const repo = urlParts[4];
5
- const branch = urlParts[6] || 'main';
6
- const folderPath = urlParts.slice(7).join('/');
2
+ // Validate the URL format
3
+ if (!url || typeof url !== 'string') {
4
+ throw new Error('Invalid URL: URL must be a non-empty string');
5
+ }
6
+
7
+ // Validate if it's a GitHub URL
8
+ const githubUrlPattern = /^https?:\/\/(?:www\.)?github\.com\/([^\/]+)\/([^\/]+)(?:\/tree\/([^\/]+)(?:\/(.+))?)?$/;
9
+ const match = url.match(githubUrlPattern);
10
+
11
+ if (!match) {
12
+ throw new Error('Invalid GitHub URL format. Expected: https://github.com/owner/repo/tree/branch/folder');
13
+ }
14
+
15
+ // Extract components from the matched pattern
16
+ const owner = match[1];
17
+ const repo = match[2];
18
+ const branch = match[3] || 'main'; // Default to 'main' if branch is not specified
19
+ const folderPath = match[4] || ''; // Empty string if no folder path
20
+
21
+ // Additional validation
22
+ if (!owner || !repo) {
23
+ throw new Error('Invalid GitHub URL: Missing repository owner or name');
24
+ }
25
+
7
26
  return { owner, repo, branch, folderPath };
8
27
  }