git-ripper 1.4.12 → 1.5.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
@@ -96,6 +96,7 @@ git-ripper https://github.com/username/repository/tree/branch/folder --zip="my-a
96
96
  | Option | Description | Default |
97
97
  | -------------------------- | ---------------------------------------- | ----------------- |
98
98
  | `-o, --output <directory>` | Specify output directory | Current directory |
99
+ | `--gh-token <token>` | GitHub Personal Access Token | - |
99
100
  | `--zip [filename]` | Create ZIP archive of downloaded content | - |
100
101
  | `--no-resume` | Disable resume functionality | - |
101
102
  | `--force-restart` | Ignore existing checkpoints and restart | - |
@@ -103,6 +104,46 @@ git-ripper https://github.com/username/repository/tree/branch/folder --zip="my-a
103
104
  | `-V, --version` | Show version number | - |
104
105
  | `-h, --help` | Show help | - |
105
106
 
107
+ ## Authentication (Private Repositories & Rate Limits)
108
+
109
+ To download from private repositories or to increase your API rate limit, you need to provide a GitHub Personal Access Token (PAT).
110
+
111
+ ### How to Generate a Token
112
+
113
+ You can use either a **Fine-grained token** (Recommended) or a **Classic token**.
114
+
115
+ #### Option A: Fine-grained Token (Recommended)
116
+
117
+ 1. Go to **Settings** > **Developer settings** > **Personal access tokens** > **Fine-grained tokens**.
118
+ 2. Click **Generate new token**.
119
+ 3. Name it (e.g., "Git-ripper").
120
+ 4. **Resource owner**: Select your user.
121
+ 5. **Repository access**: Select **Only select repositories** and choose the private repository you want to download from.
122
+ 6. **Permissions**:
123
+ * Click on **Repository permissions**.
124
+ * Find **Contents** and change Access to **Read-only**.
125
+ * *Note: Metadata permission is selected automatically.*
126
+ 7. Click **Generate token**.
127
+
128
+ #### Option B: Classic Token
129
+
130
+ 1. Go to **Settings** > **Developer settings** > **Personal access tokens** > **Tokens (classic)**.
131
+ 2. Click **Generate new token** > **Generate new token (classic)**.
132
+ 3. Give your token a descriptive name.
133
+ 4. **Select Scopes:**
134
+ * **For Private Repositories:** Select the **`repo`** scope (Full control of private repositories).
135
+ 5. Click **Generate token**.
136
+
137
+ ### Using the Token
138
+
139
+ Pass the token using the `--gh-token` flag:
140
+
141
+ ```bash
142
+ git-ripper https://github.com/username/private-repo/tree/main/src --gh-token ghp_YourTokenHere
143
+ ```
144
+
145
+ > **Security Note:** Be careful not to share your token or commit it to public repositories.
146
+
106
147
  ## Examples
107
148
 
108
149
  ### Extract a Component Library
@@ -207,7 +248,7 @@ The resume functionality uses checkpoint files stored in `.git_ripper_checkpoint
207
248
 
208
249
  ## Configuration
209
250
 
210
- Git-ripper works out of the box without configuration. For rate-limited GitHub API usage, authentication support is under development.
251
+ Git-ripper works out of the box without configuration. For rate-limited GitHub API usage or private repositories, use the `--gh-token` option as described in the [Authentication](#authentication-private-repositories--rate-limits) section.
211
252
 
212
253
  ## Troubleshooting
213
254
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-ripper",
3
- "version": "1.4.12",
3
+ "version": "1.5.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",
@@ -44,7 +44,7 @@
44
44
  "bugs": {
45
45
  "url": "https://github.com/sairajB/git-ripper/issues"
46
46
  },
47
- "homepage": "https://git-ripper.vercel.app",
47
+ "homepage": "https://git-ripper.sairajb.tech/",
48
48
  "engines": {
49
49
  "node": ">=16.0.0"
50
50
  },
package/src/archiver.js CHANGED
@@ -128,12 +128,14 @@ export const createArchive = (sourceDir, outputPath) => {
128
128
  * @param {object} repoInfo - Repository information object
129
129
  * @param {string} outputDir - Directory where files should be downloaded
130
130
  * @param {string} archiveName - Custom name for the archive file (optional)
131
+ * @param {object} [options] - Download options including token
131
132
  * @returns {Promise<string>} - Path to the created archive
132
133
  */
133
134
  export const downloadAndArchive = async (
134
135
  repoInfo,
135
136
  outputDir,
136
- archiveName = null
137
+ archiveName = null,
138
+ options = {}
137
139
  ) => {
138
140
  const { downloadFolder } = await import("./downloader.js");
139
141
 
@@ -146,7 +148,7 @@ export const downloadAndArchive = async (
146
148
  fs.mkdirSync(tempDir, { recursive: true });
147
149
  try {
148
150
  // Download the folder contents
149
- const downloadResult = await downloadFolder(repoInfo, tempDir);
151
+ const downloadResult = await downloadFolder(repoInfo, tempDir, options);
150
152
 
151
153
  // Check if download failed
152
154
  if (downloadResult && !downloadResult.success) {
package/src/downloader.js CHANGED
@@ -45,10 +45,16 @@ const getSpinnerFrame = () => {
45
45
  * @param {string} repo - Repository name
46
46
  * @param {string} branch - Branch name
47
47
  * @param {string} folderPath - Path to the folder
48
+ * @param {string} [token] - GitHub Personal Access Token
48
49
  * @returns {Promise<Array>} - Promise resolving to an array of file objects
49
50
  * @throws {Error} - Throws error on API failures instead of returning empty array
50
51
  */
51
- const fetchFolderContents = async (owner, repo, branch, folderPath) => {
52
+ const fetchFolderContents = async (owner, repo, branch, folderPath, token) => {
53
+ const headers = {
54
+ Accept: "application/vnd.github+json",
55
+ "X-GitHub-Api-Version": "2022-11-28",
56
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
57
+ };
52
58
  let effectiveBranch = branch;
53
59
  if (!effectiveBranch) {
54
60
  // If no branch is specified, fetch the default branch for the repository
@@ -56,7 +62,7 @@ const fetchFolderContents = async (owner, repo, branch, folderPath) => {
56
62
  const repoInfoUrl = `https://api.github.com/repos/${encodeURIComponent(
57
63
  owner
58
64
  )}/${encodeURIComponent(repo)}`;
59
- const repoInfoResponse = await axios.get(repoInfoUrl);
65
+ const repoInfoResponse = await axios.get(repoInfoUrl, { headers });
60
66
  effectiveBranch = repoInfoResponse.data.default_branch;
61
67
  if (!effectiveBranch) {
62
68
  throw new Error(
@@ -85,7 +91,7 @@ const fetchFolderContents = async (owner, repo, branch, folderPath) => {
85
91
  )}?recursive=1`;
86
92
 
87
93
  try {
88
- const response = await axios.get(apiUrl);
94
+ const response = await axios.get(apiUrl, { headers });
89
95
 
90
96
  // Check if GitHub API returned truncated results
91
97
  if (response.data.truncated) {
@@ -162,9 +168,13 @@ const fetchFolderContents = async (owner, repo, branch, folderPath) => {
162
168
  * @param {string} branch - Branch name
163
169
  * @param {string} filePath - Path to the file
164
170
  * @param {string} outputPath - Path where the file should be saved
171
+ * @param {string} [token] - GitHub Personal Access Token
165
172
  * @returns {Promise<Object>} - Object containing download status
166
173
  */
167
- const downloadFile = async (owner, repo, branch, filePath, outputPath) => {
174
+ const downloadFile = async (owner, repo, branch, filePath, outputPath, token) => {
175
+ const headers = {
176
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
177
+ };
168
178
  let effectiveBranch = branch;
169
179
  if (!effectiveBranch) {
170
180
  // If no branch is specified, fetch the default branch for the repository
@@ -174,7 +184,7 @@ const downloadFile = async (owner, repo, branch, filePath, outputPath) => {
174
184
  const repoInfoUrl = `https://api.github.com/repos/${encodeURIComponent(
175
185
  owner
176
186
  )}/${encodeURIComponent(repo)}`;
177
- const repoInfoResponse = await axios.get(repoInfoUrl);
187
+ const repoInfoResponse = await axios.get(repoInfoUrl, { headers });
178
188
  effectiveBranch = repoInfoResponse.data.default_branch;
179
189
  if (!effectiveBranch) {
180
190
  // console.error(chalk.red(`Could not determine default branch for ${owner}/${repo} for file ${filePath}.`));
@@ -213,7 +223,7 @@ const downloadFile = async (owner, repo, branch, filePath, outputPath) => {
213
223
  const url = `${baseUrl}${fileUrlPath}`;
214
224
 
215
225
  try {
216
- const response = await axios.get(url, { responseType: "arraybuffer" });
226
+ const response = await axios.get(url, { responseType: "arraybuffer", headers });
217
227
 
218
228
  // Ensure the directory exists
219
229
  try {
@@ -338,18 +348,22 @@ const createProgressRenderer = (owner, repo, folderPath) => {
338
348
  * @param {string} repoInfo.branch - Branch name
339
349
  * @param {string} repoInfo.folderPath - Path to the folder
340
350
  * @param {string} outputDir - Directory where files should be saved
351
+ * @param {Object} [options] - Download options
352
+ * @param {string} [options.token] - GitHub Personal Access Token
341
353
  * @returns {Promise<void>} - Promise that resolves when all files are downloaded
342
354
  */
343
355
  const downloadFolder = async (
344
356
  { owner, repo, branch, folderPath },
345
- outputDir
357
+ outputDir,
358
+ options = {}
346
359
  ) => {
360
+ const { token } = options;
347
361
  console.log(
348
362
  chalk.cyan(`Analyzing repository structure for ${owner}/${repo}...`)
349
363
  );
350
364
 
351
365
  try {
352
- const contents = await fetchFolderContents(owner, repo, branch, folderPath);
366
+ const contents = await fetchFolderContents(owner, repo, branch, folderPath, token);
353
367
 
354
368
  if (!contents || contents.length === 0) {
355
369
  const message = `No files found in ${folderPath || "repository root"}`;
@@ -428,7 +442,8 @@ const downloadFolder = async (
428
442
  repo,
429
443
  branch,
430
444
  item.path,
431
- outputFilePath
445
+ outputFilePath,
446
+ token
432
447
  );
433
448
 
434
449
  // Update progress metrics
@@ -537,7 +552,7 @@ const downloadFolder = async (
537
552
  };
538
553
 
539
554
  // Export functions in ESM format
540
- export { downloadFolder, downloadFolderWithResume };
555
+ export { downloadFolder, downloadFolderWithResume, downloadFile };
541
556
 
542
557
  /**
543
558
  * Downloads all files from a folder in a GitHub repository with resume capability
@@ -547,10 +562,10 @@ const downloadFolderWithResume = async (
547
562
  outputDir,
548
563
  options = { resume: true, forceRestart: false }
549
564
  ) => {
550
- const { resume = true, forceRestart = false } = options;
565
+ const { resume = true, forceRestart = false, token } = options;
551
566
 
552
567
  if (!resume) {
553
- return downloadFolder({ owner, repo, branch, folderPath }, outputDir);
568
+ return downloadFolder({ owner, repo, branch, folderPath }, outputDir, options);
554
569
  }
555
570
 
556
571
  const resumeManager = new ResumeManager();
@@ -622,7 +637,7 @@ const downloadFolderWithResume = async (
622
637
  );
623
638
 
624
639
  try {
625
- const contents = await fetchFolderContents(owner, repo, branch, folderPath);
640
+ const contents = await fetchFolderContents(owner, repo, branch, folderPath, token);
626
641
  if (!contents || contents.length === 0) {
627
642
  const message = `No files found in ${folderPath || "repository root"}`;
628
643
  console.log(chalk.yellow(message));
@@ -741,7 +756,8 @@ const downloadFolderWithResume = async (
741
756
  repo,
742
757
  branch,
743
758
  item.path,
744
- outputFilePath
759
+ outputFilePath,
760
+ token
745
761
  );
746
762
 
747
763
  if (result.success) {
package/src/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { program } from "commander";
2
2
  import { parseGitHubUrl } from "./parser.js";
3
- import { downloadFolder, downloadFolderWithResume } from "./downloader.js";
3
+ import { downloadFolder, downloadFolderWithResume, downloadFile } from "./downloader.js";
4
4
  import { downloadAndArchive } from "./archiver.js";
5
5
  import { ResumeManager } from "./resumeManager.js";
6
6
  import { fileURLToPath } from "node:url";
7
- import { dirname, join, resolve } from "node:path";
7
+ import { dirname, join, resolve, basename } from "node:path";
8
8
  import fs from "node:fs";
9
9
  import process from "node:process";
10
10
  import chalk from "chalk";
@@ -61,6 +61,7 @@ const initializeCLI = () => {
61
61
  .description("Clone specific folders from GitHub repositories")
62
62
  .argument("[url]", "GitHub URL of the folder to clone")
63
63
  .option("-o, --output <directory>", "Output directory", process.cwd())
64
+ .option("--gh-token <token>", "GitHub Personal Access Token for private repositories")
64
65
  .option("--zip [filename]", "Create ZIP archive of downloaded files")
65
66
  .option("--no-resume", "Disable resume functionality")
66
67
  .option("--force-restart", "Ignore existing checkpoints and start fresh")
@@ -122,6 +123,7 @@ const initializeCLI = () => {
122
123
  const downloadOptions = {
123
124
  resume: options.resume !== false, // Default to true unless --no-resume
124
125
  forceRestart: options.forceRestart || false,
126
+ token: options.ghToken,
125
127
  };
126
128
 
127
129
  let operationType = createArchive ? "archive" : "download";
@@ -131,7 +133,19 @@ const initializeCLI = () => {
131
133
  try {
132
134
  if (createArchive) {
133
135
  console.log(`Creating ZIP archive...`);
134
- await downloadAndArchive(parsedUrl, options.output, archiveName);
136
+ await downloadAndArchive(parsedUrl, options.output, archiveName, downloadOptions);
137
+ } else if (parsedUrl.type === "blob") {
138
+ console.log(`Downloading file to: ${options.output}`);
139
+ const fileName = basename(parsedUrl.folderPath);
140
+ const outputPath = join(options.output, fileName);
141
+ result = await downloadFile(
142
+ parsedUrl.owner,
143
+ parsedUrl.repo,
144
+ parsedUrl.branch,
145
+ parsedUrl.folderPath,
146
+ outputPath,
147
+ options.ghToken
148
+ );
135
149
  } else {
136
150
  console.log(`Downloading folder to: ${options.output}`);
137
151
  if (downloadOptions.resume) {
@@ -141,7 +155,7 @@ const initializeCLI = () => {
141
155
  downloadOptions
142
156
  );
143
157
  } else {
144
- result = await downloadFolder(parsedUrl, options.output);
158
+ result = await downloadFolder(parsedUrl, options.output, downloadOptions);
145
159
  }
146
160
  }
147
161
  } catch (opError) {
package/src/parser.js CHANGED
@@ -6,7 +6,7 @@ export function parseGitHubUrl(url) {
6
6
 
7
7
  // Validate if it's a GitHub URL
8
8
  const githubUrlPattern =
9
- /^https?:\/\/(?:www\.)?github\.com\/([^\/]+)\/([^\/]+)(?:\/(?:tree|blob)\/([^\/]+)(?:\/(.+))?)?$/;
9
+ /^https?:\/\/(?:www\.)?github\.com\/([^\/]+)\/([^\/]+)(?:\/(tree|blob)\/([^\/]+)(?:\/(.+))?)?$/;
10
10
  const match = url.match(githubUrlPattern);
11
11
 
12
12
  if (!match) {
@@ -17,14 +17,21 @@ export function parseGitHubUrl(url) {
17
17
 
18
18
  // Extract components from the matched pattern
19
19
  const owner = decodeURIComponent(match[1]);
20
- const repo = decodeURIComponent(match[2]);
21
- const branch = match[3] ? decodeURIComponent(match[3]) : ""; // Branch is an empty string if not present
22
- const folderPath = match[4] ? decodeURIComponent(match[4]) : ""; // Empty string if no folder path
20
+ let repo = decodeURIComponent(match[2]);
21
+
22
+ // Remove .git suffix if present
23
+ if (repo.endsWith(".git")) {
24
+ repo = repo.slice(0, -4);
25
+ }
26
+
27
+ const type = match[3] || "tree"; // Default to tree if not present (root of repo)
28
+ const branch = match[4] ? decodeURIComponent(match[4]) : ""; // Branch is an empty string if not present
29
+ const folderPath = match[5] ? decodeURIComponent(match[5]) : ""; // Empty string if no folder path
23
30
 
24
31
  // Additional validation
25
32
  if (!owner || !repo) {
26
33
  throw new Error("Invalid GitHub URL: Missing repository owner or name");
27
34
  }
28
35
 
29
- return { owner, repo, branch, folderPath };
36
+ return { owner, repo, branch, folderPath, type };
30
37
  }