git-ripper 1.5.3 → 1.6.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
@@ -94,14 +94,16 @@ git-ripper https://github.com/username/repository/tree/branch/folder --zip="my-a
94
94
  ### Command Line Options
95
95
 
96
96
  | Option | Description | Default |
97
- | -------------------------- | ---------------------------------------- | ----------------- |
97
+ | -------------------------- | ---------------------------------------- | ----------------- | --- | -------------------------- | ------------------------- | --- |
98
98
  | `-o, --output <directory>` | Specify output directory | Current directory |
99
99
  | `--gh-token <token>` | GitHub Personal Access Token | - |
100
100
  | `--zip [filename]` | Create ZIP archive of downloaded content | - |
101
101
  | `--no-resume` | Disable resume functionality | - |
102
102
  | `--force-restart` | Ignore existing checkpoints and restart | - |
103
- | `--list-checkpoints` | List all saved download checkpoints | - |
104
- | `-V, --version` | Show version number | - |
103
+ | `--list-checkpoints` | List all saved download checkpoints | - | | `config set-token <token>` | Save GitHub token locally | - |
104
+ | `config get-token` | Show saved token (masked) | - |
105
+ | `config remove-token` | Remove saved token | - |
106
+ | `config show` | Show current configuration | - | | `-V, --version` | Show version number | - |
105
107
  | `-h, --help` | Show help | - |
106
108
 
107
109
  ## Authentication (Private Repositories & Rate Limits)
@@ -136,12 +138,62 @@ You can use either a **Fine-grained token** (Recommended) or a **Classic token**
136
138
 
137
139
  ### Using the Token
138
140
 
141
+ #### Option 1: Save Token Locally (Recommended)
142
+
143
+ Save your token once and use it automatically for all future downloads:
144
+
145
+ ```bash
146
+ # Save the token
147
+ git-ripper config set-token ghp_YourTokenHere
148
+
149
+ # Now just download - token is used automatically
150
+ git-ripper https://github.com/username/private-repo/tree/main/src
151
+ ```
152
+
153
+ #### Option 2: Environment Variable
154
+
155
+ Set the `GIT_RIPPER_TOKEN` environment variable:
156
+
157
+ ```bash
158
+ # Windows (PowerShell)
159
+ $env:GIT_RIPPER_TOKEN = "ghp_YourTokenHere"
160
+
161
+ # Linux/Mac
162
+ export GIT_RIPPER_TOKEN="ghp_YourTokenHere"
163
+
164
+ # Then download
165
+ git-ripper https://github.com/username/private-repo/tree/main/src
166
+ ```
167
+
168
+ #### Option 3: Command Line Flag
169
+
139
170
  Pass the token using the `--gh-token` flag:
140
171
 
141
172
  ```bash
142
173
  git-ripper https://github.com/username/private-repo/tree/main/src --gh-token ghp_YourTokenHere
143
174
  ```
144
175
 
176
+ #### Token Priority
177
+
178
+ When multiple tokens are available, git-ripper uses this priority:
179
+
180
+ 1. `--gh-token` command line flag (highest)
181
+ 2. `GIT_RIPPER_TOKEN` environment variable
182
+ 3. Saved token from `config set-token`
183
+
184
+ #### Managing Saved Tokens
185
+
186
+ ```bash
187
+ # View current configuration
188
+ git-ripper config show
189
+
190
+ # View saved token (masked)
191
+ git-ripper config get-token
192
+
193
+ # Remove saved token
194
+ git-ripper config remove-token
195
+ ```
196
+
145
197
  > **Security Note:** Be careful not to share your token or commit it to public repositories.
146
198
 
147
199
  ## Examples
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-ripper",
3
- "version": "1.5.3",
3
+ "version": "1.6.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",
@@ -0,0 +1,160 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import chalk from "chalk";
5
+
6
+ /**
7
+ * Configuration manager for git-ripper
8
+ * Handles persistent storage of settings like GitHub tokens
9
+ */
10
+ class ConfigManager {
11
+ constructor() {
12
+ this.configDir = path.join(os.homedir(), ".git-ripper");
13
+ this.configFile = path.join(this.configDir, "config.json");
14
+ }
15
+
16
+ /**
17
+ * Ensures the config directory exists
18
+ * @private
19
+ */
20
+ _ensureConfigDir() {
21
+ if (!fs.existsSync(this.configDir)) {
22
+ fs.mkdirSync(this.configDir, { recursive: true, mode: 0o700 });
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Loads the configuration from disk
28
+ * @returns {Object} - The configuration object
29
+ */
30
+ loadConfig() {
31
+ try {
32
+ if (fs.existsSync(this.configFile)) {
33
+ const data = fs.readFileSync(this.configFile, "utf8");
34
+ return JSON.parse(data);
35
+ }
36
+ } catch (error) {
37
+ // If config is corrupted, return empty config
38
+ console.warn(
39
+ chalk.yellow(`Warning: Could not read config file: ${error.message}`),
40
+ );
41
+ }
42
+ return {};
43
+ }
44
+
45
+ /**
46
+ * Saves the configuration to disk
47
+ * @param {Object} config - The configuration object to save
48
+ */
49
+ saveConfig(config) {
50
+ this._ensureConfigDir();
51
+ try {
52
+ fs.writeFileSync(this.configFile, JSON.stringify(config, null, 2), {
53
+ encoding: "utf8",
54
+ mode: 0o600, // Read/write for owner only
55
+ });
56
+ } catch (error) {
57
+ throw new Error(`Failed to save config: ${error.message}`);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Sets the GitHub token in the config
63
+ * @param {string} token - The GitHub Personal Access Token
64
+ */
65
+ setToken(token) {
66
+ if (!token || typeof token !== "string" || token.trim() === "") {
67
+ throw new Error("Token cannot be empty");
68
+ }
69
+
70
+ const config = this.loadConfig();
71
+ config.github_token = token.trim();
72
+ config.token_saved_at = new Date().toISOString();
73
+ this.saveConfig(config);
74
+ }
75
+
76
+ /**
77
+ * Gets the GitHub token from config
78
+ * @returns {string|null} - The token or null if not set
79
+ */
80
+ getToken() {
81
+ const config = this.loadConfig();
82
+ return config.github_token || null;
83
+ }
84
+
85
+ /**
86
+ * Removes the GitHub token from config
87
+ * @returns {boolean} - True if token was removed, false if no token existed
88
+ */
89
+ removeToken() {
90
+ const config = this.loadConfig();
91
+ if (config.github_token) {
92
+ delete config.github_token;
93
+ delete config.token_saved_at;
94
+ this.saveConfig(config);
95
+ return true;
96
+ }
97
+ return false;
98
+ }
99
+
100
+ /**
101
+ * Gets the token with priority: CLI flag > env var > stored config
102
+ * @param {string|null} cliToken - Token passed via CLI flag
103
+ * @returns {string|null} - The resolved token or null
104
+ */
105
+ resolveToken(cliToken = null) {
106
+ // Priority 1: CLI flag
107
+ if (cliToken) {
108
+ return cliToken;
109
+ }
110
+
111
+ // Priority 2: Environment variable
112
+ const envToken = process.env.GIT_RIPPER_TOKEN;
113
+ if (envToken) {
114
+ return envToken;
115
+ }
116
+
117
+ // Priority 3: Stored config
118
+ return this.getToken();
119
+ }
120
+
121
+ /**
122
+ * Masks a token for display (shows first 4 and last 4 characters)
123
+ * @param {string} token - The token to mask
124
+ * @returns {string} - The masked token
125
+ */
126
+ maskToken(token) {
127
+ if (!token || token.length < 12) {
128
+ return "****";
129
+ }
130
+ return `${token.substring(0, 4)}${"*".repeat(token.length - 8)}${token.substring(token.length - 4)}`;
131
+ }
132
+
133
+ /**
134
+ * Shows the current configuration
135
+ * @returns {Object} - Configuration summary
136
+ */
137
+ showConfig() {
138
+ const config = this.loadConfig();
139
+ const envToken = process.env.GIT_RIPPER_TOKEN;
140
+
141
+ return {
142
+ hasStoredToken: !!config.github_token,
143
+ maskedToken:
144
+ config.github_token ? this.maskToken(config.github_token) : null,
145
+ tokenSavedAt: config.token_saved_at || null,
146
+ hasEnvToken: !!envToken,
147
+ configPath: this.configFile,
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Gets the path to the config file
153
+ * @returns {string} - The config file path
154
+ */
155
+ getConfigPath() {
156
+ return this.configFile;
157
+ }
158
+ }
159
+
160
+ export { ConfigManager };
package/src/index.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  } from "./downloader.js";
8
8
  import { downloadAndArchive } from "./archiver.js";
9
9
  import { ResumeManager } from "./resumeManager.js";
10
+ import { ConfigManager } from "./configManager.js";
10
11
  import { fileURLToPath } from "node:url";
11
12
  import { dirname, join, resolve, basename } from "node:path";
12
13
  import fs from "node:fs";
@@ -61,6 +62,90 @@ const validateOutputDirectory = (outputDir) => {
61
62
  };
62
63
 
63
64
  const initializeCLI = () => {
65
+ const configManager = new ConfigManager();
66
+
67
+ // Add config subcommand
68
+ const configCmd = program
69
+ .command("config")
70
+ .description("Manage git-ripper configuration");
71
+
72
+ configCmd
73
+ .command("set-token <token>")
74
+ .description("Save GitHub Personal Access Token locally")
75
+ .action((token) => {
76
+ try {
77
+ configManager.setToken(token);
78
+ console.log(chalk.green("Token saved successfully!"));
79
+ console.log(
80
+ chalk.yellow(
81
+ "Security note: Keep your config file secure. Location: " +
82
+ configManager.getConfigPath(),
83
+ ),
84
+ );
85
+ } catch (error) {
86
+ console.error(chalk.red(`Error: ${error.message}`));
87
+ process.exit(1);
88
+ }
89
+ });
90
+
91
+ configCmd
92
+ .command("get-token")
93
+ .description("Show saved GitHub token (masked)")
94
+ .action(() => {
95
+ const token = configManager.getToken();
96
+ if (token) {
97
+ console.log(`Saved token: ${configManager.maskToken(token)}`);
98
+ } else {
99
+ console.log(chalk.yellow("No token saved."));
100
+ console.log(
101
+ "Use 'git-ripper config set-token <token>' to save a token.",
102
+ );
103
+ }
104
+ });
105
+
106
+ configCmd
107
+ .command("remove-token")
108
+ .description("Remove saved GitHub token")
109
+ .action(() => {
110
+ if (configManager.removeToken()) {
111
+ console.log(chalk.green("Token removed successfully."));
112
+ } else {
113
+ console.log(chalk.yellow("No token was saved."));
114
+ }
115
+ });
116
+
117
+ configCmd
118
+ .command("show")
119
+ .description("Show current configuration")
120
+ .action(() => {
121
+ const config = configManager.showConfig();
122
+ console.log(chalk.cyan("\nGit-ripper Configuration:"));
123
+ console.log(` Config file: ${config.configPath}`);
124
+ console.log(
125
+ ` Stored token: ${
126
+ config.hasStoredToken ?
127
+ chalk.green(config.maskedToken)
128
+ : chalk.gray("Not set")
129
+ }`,
130
+ );
131
+ if (config.tokenSavedAt) {
132
+ console.log(
133
+ ` Token saved: ${new Date(config.tokenSavedAt).toLocaleString()}`,
134
+ );
135
+ }
136
+ console.log(
137
+ ` Environment token (GIT_RIPPER_TOKEN): ${
138
+ config.hasEnvToken ? chalk.green("Set") : chalk.gray("Not set")
139
+ }`,
140
+ );
141
+ console.log(
142
+ chalk.gray(
143
+ "\n Token priority: --gh-token flag > GIT_RIPPER_TOKEN env > stored token",
144
+ ),
145
+ );
146
+ console.log();
147
+ });
148
+
64
149
  program
65
150
  .version(packageJson.version)
66
151
  .description("Clone specific folders from GitHub repositories")
@@ -187,11 +272,21 @@ const initializeCLI = () => {
187
272
  const archiveName =
188
273
  typeof options.zip === "string" ? options.zip : null;
189
274
 
275
+ // Resolve token with priority: CLI flag > env var > stored config
276
+ const resolvedToken = configManager.resolveToken(options.ghToken);
277
+ if (resolvedToken && !options.ghToken) {
278
+ const source =
279
+ process.env.GIT_RIPPER_TOKEN ?
280
+ "environment variable"
281
+ : "saved config";
282
+ console.log(chalk.gray(`Using GitHub token from ${source}`));
283
+ }
284
+
190
285
  // Prepare download options
191
286
  const downloadOptions = {
192
287
  resume: options.resume !== false, // Default to true unless --no-resume
193
288
  forceRestart: options.forceRestart || false,
194
- token: options.ghToken,
289
+ token: resolvedToken,
195
290
  };
196
291
 
197
292
  let operationType = createArchive ? "archive" : "download";
@@ -217,7 +312,7 @@ const initializeCLI = () => {
217
312
  parsedUrl.branch,
218
313
  parsedUrl.folderPath,
219
314
  outputPath,
220
- options.ghToken,
315
+ resolvedToken,
221
316
  );
222
317
  } else {
223
318
  console.log(`Downloading folder to: ${options.output}`);