git-ripper 1.5.1 → 1.5.2

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/src/index.js CHANGED
@@ -1,195 +1,195 @@
1
- import { program } from "commander";
2
- import { parseGitHubUrl } from "./parser.js";
3
- import { downloadFolder, downloadFolderWithResume, downloadFile } from "./downloader.js";
4
- import { downloadAndArchive } from "./archiver.js";
5
- import { ResumeManager } from "./resumeManager.js";
6
- import { fileURLToPath } from "node:url";
7
- import { dirname, join, resolve, basename } from "node:path";
8
- import fs from "node:fs";
9
- import process from "node:process";
10
- import chalk from "chalk";
11
-
12
- // Get package.json for version
13
- const __filename = fileURLToPath(import.meta.url);
14
- const __dirname = dirname(__filename);
15
- const packagePath = join(__dirname, "..", "package.json");
16
- const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
17
-
18
- /**
19
- * Validates and ensures the output directory exists
20
- * @param {string} outputDir - The directory path to validate
21
- * @returns {string} - The resolved directory path
22
- * @throws {Error} - If the directory is invalid or cannot be created
23
- */
24
- const validateOutputDirectory = (outputDir) => {
25
- if (!outputDir) {
26
- throw new Error("Output directory is required");
27
- }
28
-
29
- // Resolve to absolute path
30
- const resolvedDir = resolve(outputDir);
31
-
32
- try {
33
- // Check if directory exists, if not try to create it
34
- if (!fs.existsSync(resolvedDir)) {
35
- fs.mkdirSync(resolvedDir, { recursive: true });
36
- } else {
37
- // Check if it's actually a directory
38
- const stats = fs.statSync(resolvedDir);
39
- if (!stats.isDirectory()) {
40
- throw new Error(
41
- `Output path exists but is not a directory: ${outputDir}`
42
- );
43
- }
44
- }
45
-
46
- // Check if the directory is writable
47
- fs.accessSync(resolvedDir, fs.constants.W_OK);
48
-
49
- return resolvedDir;
50
- } catch (error) {
51
- if (error.code === "EACCES") {
52
- throw new Error(`Permission denied: Cannot write to ${outputDir}`);
53
- }
54
- throw new Error(`Invalid output directory: ${error.message}`);
55
- }
56
- };
57
-
58
- const initializeCLI = () => {
59
- program
60
- .version(packageJson.version)
61
- .description("Clone specific folders from GitHub repositories")
62
- .argument("[url]", "GitHub URL of the folder to clone")
63
- .option("-o, --output <directory>", "Output directory", process.cwd())
64
- .option("--gh-token <token>", "GitHub Personal Access Token for private repositories")
65
- .option("--zip [filename]", "Create ZIP archive of downloaded files")
66
- .option("--no-resume", "Disable resume functionality")
67
- .option("--force-restart", "Ignore existing checkpoints and start fresh")
68
- .option("--list-checkpoints", "List all existing download checkpoints")
69
- .action(async (url, options) => {
70
- try {
71
- // Handle list checkpoints option
72
- if (options.listCheckpoints) {
73
- const resumeManager = new ResumeManager();
74
- const checkpoints = resumeManager.listCheckpoints();
75
-
76
- if (checkpoints.length === 0) {
77
- console.log(chalk.yellow("No download checkpoints found."));
78
- return;
79
- }
80
-
81
- console.log(chalk.cyan("\nDownload Checkpoints:"));
82
- checkpoints.forEach((cp, index) => {
83
- console.log(chalk.blue(`\n${index + 1}. ID: ${cp.id}`));
84
- console.log(` URL: ${cp.url}`);
85
- console.log(` Output: ${cp.outputDir}`);
86
- console.log(` Progress: ${cp.progress}`);
87
- console.log(
88
- ` Last Updated: ${new Date(cp.timestamp).toLocaleString()}`
89
- );
90
- if (cp.failedFiles > 0) {
91
- console.log(chalk.yellow(` Failed Files: ${cp.failedFiles}`));
92
- }
93
- });
94
- console.log();
95
- return;
96
- }
97
-
98
- // URL is required for download operations
99
- if (!url) {
100
- console.error(
101
- chalk.red("Error: URL is required for download operations")
102
- );
103
- console.log("Use --list-checkpoints to see existing downloads");
104
- process.exit(1);
105
- }
106
-
107
- console.log(`Parsing URL: ${url}`);
108
- const parsedUrl = parseGitHubUrl(url);
109
-
110
- // Validate output directory
111
- try {
112
- options.output = validateOutputDirectory(options.output);
113
- } catch (dirError) {
114
- throw new Error(`Output directory error: ${dirError.message}`);
115
- }
116
-
117
- // Handle archive option
118
- const createArchive = options.zip !== undefined;
119
- const archiveName =
120
- typeof options.zip === "string" ? options.zip : null;
121
-
122
- // Prepare download options
123
- const downloadOptions = {
124
- resume: options.resume !== false, // Default to true unless --no-resume
125
- forceRestart: options.forceRestart || false,
126
- token: options.ghToken,
127
- };
128
-
129
- let operationType = createArchive ? "archive" : "download";
130
- let result = null;
131
- let error = null;
132
-
133
- try {
134
- if (createArchive) {
135
- console.log(`Creating ZIP archive...`);
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
- );
149
- } else {
150
- console.log(`Downloading folder to: ${options.output}`);
151
- if (downloadOptions.resume) {
152
- result = await downloadFolderWithResume(
153
- parsedUrl,
154
- options.output,
155
- downloadOptions
156
- );
157
- } else {
158
- result = await downloadFolder(parsedUrl, options.output, downloadOptions);
159
- }
160
- }
161
- } catch (opError) {
162
- error = opError;
163
- }
164
-
165
- // Consolidated result and error handling
166
- if (error) {
167
- const failMsg =
168
- operationType === "archive"
169
- ? `Archive creation failed: ${error.message}`
170
- : `Download failed: ${error.message}`;
171
- console.error(chalk.red(failMsg));
172
- process.exit(1);
173
- } else if (!createArchive && result && !result.success) {
174
- console.error(chalk.red(`Download failed`));
175
- process.exit(1);
176
- } else if (!createArchive && result && result.isEmpty) {
177
- console.log("Operation completed - no files to download!");
178
- } else {
179
- console.log("Operation completed successfully!");
180
- }
181
- } catch (error) {
182
- console.error("Error:", error.message);
183
- process.exit(1);
184
- }
185
- });
186
-
187
- program.parse(process.argv);
188
- };
189
-
190
- // Ensure function is executed when run directly
191
- if (import.meta.url === `file://${process.argv[1]}`) {
192
- initializeCLI();
193
- }
194
-
195
- export { initializeCLI, downloadFolder };
1
+ import { program } from "commander";
2
+ import { parseGitHubUrl } from "./parser.js";
3
+ import { downloadFolder, downloadFolderWithResume, downloadFile } from "./downloader.js";
4
+ import { downloadAndArchive } from "./archiver.js";
5
+ import { ResumeManager } from "./resumeManager.js";
6
+ import { fileURLToPath } from "node:url";
7
+ import { dirname, join, resolve, basename } from "node:path";
8
+ import fs from "node:fs";
9
+ import process from "node:process";
10
+ import chalk from "chalk";
11
+
12
+ // Get package.json for version
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+ const packagePath = join(__dirname, "..", "package.json");
16
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
17
+
18
+ /**
19
+ * Validates and ensures the output directory exists
20
+ * @param {string} outputDir - The directory path to validate
21
+ * @returns {string} - The resolved directory path
22
+ * @throws {Error} - If the directory is invalid or cannot be created
23
+ */
24
+ const validateOutputDirectory = (outputDir) => {
25
+ if (!outputDir) {
26
+ throw new Error("Output directory is required");
27
+ }
28
+
29
+ // Resolve to absolute path
30
+ const resolvedDir = resolve(outputDir);
31
+
32
+ try {
33
+ // Check if directory exists, if not try to create it
34
+ if (!fs.existsSync(resolvedDir)) {
35
+ fs.mkdirSync(resolvedDir, { recursive: true });
36
+ } else {
37
+ // Check if it's actually a directory
38
+ const stats = fs.statSync(resolvedDir);
39
+ if (!stats.isDirectory()) {
40
+ throw new Error(
41
+ `Output path exists but is not a directory: ${outputDir}`
42
+ );
43
+ }
44
+ }
45
+
46
+ // Check if the directory is writable
47
+ fs.accessSync(resolvedDir, fs.constants.W_OK);
48
+
49
+ return resolvedDir;
50
+ } catch (error) {
51
+ if (error.code === "EACCES") {
52
+ throw new Error(`Permission denied: Cannot write to ${outputDir}`);
53
+ }
54
+ throw new Error(`Invalid output directory: ${error.message}`);
55
+ }
56
+ };
57
+
58
+ const initializeCLI = () => {
59
+ program
60
+ .version(packageJson.version)
61
+ .description("Clone specific folders from GitHub repositories")
62
+ .argument("[url]", "GitHub URL of the folder to clone")
63
+ .option("-o, --output <directory>", "Output directory", process.cwd())
64
+ .option("--gh-token <token>", "GitHub Personal Access Token for private repositories")
65
+ .option("--zip [filename]", "Create ZIP archive of downloaded files")
66
+ .option("--no-resume", "Disable resume functionality")
67
+ .option("--force-restart", "Ignore existing checkpoints and start fresh")
68
+ .option("--list-checkpoints", "List all existing download checkpoints")
69
+ .action(async (url, options) => {
70
+ try {
71
+ // Handle list checkpoints option
72
+ if (options.listCheckpoints) {
73
+ const resumeManager = new ResumeManager();
74
+ const checkpoints = resumeManager.listCheckpoints();
75
+
76
+ if (checkpoints.length === 0) {
77
+ console.log(chalk.yellow("No download checkpoints found."));
78
+ return;
79
+ }
80
+
81
+ console.log(chalk.cyan("\nDownload Checkpoints:"));
82
+ checkpoints.forEach((cp, index) => {
83
+ console.log(chalk.blue(`\n${index + 1}. ID: ${cp.id}`));
84
+ console.log(` URL: ${cp.url}`);
85
+ console.log(` Output: ${cp.outputDir}`);
86
+ console.log(` Progress: ${cp.progress}`);
87
+ console.log(
88
+ ` Last Updated: ${new Date(cp.timestamp).toLocaleString()}`
89
+ );
90
+ if (cp.failedFiles > 0) {
91
+ console.log(chalk.yellow(` Failed Files: ${cp.failedFiles}`));
92
+ }
93
+ });
94
+ console.log();
95
+ return;
96
+ }
97
+
98
+ // URL is required for download operations
99
+ if (!url) {
100
+ console.error(
101
+ chalk.red("Error: URL is required for download operations")
102
+ );
103
+ console.log("Use --list-checkpoints to see existing downloads");
104
+ process.exit(1);
105
+ }
106
+
107
+ console.log(`Parsing URL: ${url}`);
108
+ const parsedUrl = parseGitHubUrl(url);
109
+
110
+ // Validate output directory
111
+ try {
112
+ options.output = validateOutputDirectory(options.output);
113
+ } catch (dirError) {
114
+ throw new Error(`Output directory error: ${dirError.message}`);
115
+ }
116
+
117
+ // Handle archive option
118
+ const createArchive = options.zip !== undefined;
119
+ const archiveName =
120
+ typeof options.zip === "string" ? options.zip : null;
121
+
122
+ // Prepare download options
123
+ const downloadOptions = {
124
+ resume: options.resume !== false, // Default to true unless --no-resume
125
+ forceRestart: options.forceRestart || false,
126
+ token: options.ghToken,
127
+ };
128
+
129
+ let operationType = createArchive ? "archive" : "download";
130
+ let result = null;
131
+ let error = null;
132
+
133
+ try {
134
+ if (createArchive) {
135
+ console.log(`Creating ZIP archive...`);
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
+ );
149
+ } else {
150
+ console.log(`Downloading folder to: ${options.output}`);
151
+ if (downloadOptions.resume) {
152
+ result = await downloadFolderWithResume(
153
+ parsedUrl,
154
+ options.output,
155
+ downloadOptions
156
+ );
157
+ } else {
158
+ result = await downloadFolder(parsedUrl, options.output, downloadOptions);
159
+ }
160
+ }
161
+ } catch (opError) {
162
+ error = opError;
163
+ }
164
+
165
+ // Consolidated result and error handling
166
+ if (error) {
167
+ const failMsg =
168
+ operationType === "archive"
169
+ ? `Archive creation failed: ${error.message}`
170
+ : `Download failed: ${error.message}`;
171
+ console.error(chalk.red(failMsg));
172
+ process.exit(1);
173
+ } else if (!createArchive && result && !result.success) {
174
+ console.error(chalk.red(`Download failed`));
175
+ process.exit(1);
176
+ } else if (!createArchive && result && result.isEmpty) {
177
+ console.log("Operation completed - no files to download!");
178
+ } else {
179
+ console.log("Operation completed successfully!");
180
+ }
181
+ } catch (error) {
182
+ console.error("Error:", error.message);
183
+ process.exit(1);
184
+ }
185
+ });
186
+
187
+ program.parse(process.argv);
188
+ };
189
+
190
+ // Ensure function is executed when run directly
191
+ if (import.meta.url === `file://${process.argv[1]}`) {
192
+ initializeCLI();
193
+ }
194
+
195
+ export { initializeCLI, downloadFolder };
package/src/parser.js CHANGED
@@ -1,37 +1,37 @@
1
- export function parseGitHubUrl(url) {
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 =
9
- /^https?:\/\/(?:www\.)?github\.com\/([^\/]+)\/([^\/]+)(?:\/(tree|blob)\/([^\/]+)(?:\/(.+))?)?$/;
10
- const match = url.match(githubUrlPattern);
11
-
12
- if (!match) {
13
- throw new Error(
14
- "Invalid GitHub URL format. Expected: https://github.com/owner/repo[/tree|/blob]/branch/folder_or_file"
15
- );
16
- }
17
-
18
- // Extract components from the matched pattern
19
- const owner = decodeURIComponent(match[1]);
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
30
-
31
- // Additional validation
32
- if (!owner || !repo) {
33
- throw new Error("Invalid GitHub URL: Missing repository owner or name");
34
- }
35
-
36
- return { owner, repo, branch, folderPath, type };
37
- }
1
+ export function parseGitHubUrl(url) {
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 =
9
+ /^https?:\/\/(?:www\.)?github\.com\/([^\/]+)\/([^\/]+)(?:\/(tree|blob)\/([^\/]+)(?:\/(.+))?)?$/;
10
+ const match = url.match(githubUrlPattern);
11
+
12
+ if (!match) {
13
+ throw new Error(
14
+ "Invalid GitHub URL format. Expected: https://github.com/owner/repo[/tree|/blob]/branch/folder_or_file"
15
+ );
16
+ }
17
+
18
+ // Extract components from the matched pattern
19
+ const owner = decodeURIComponent(match[1]);
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
30
+
31
+ // Additional validation
32
+ if (!owner || !repo) {
33
+ throw new Error("Invalid GitHub URL: Missing repository owner or name");
34
+ }
35
+
36
+ return { owner, repo, branch, folderPath, type };
37
+ }